Configuration as code

Three YAML files describe an operator.

An operator's whole setup is three kinds of YAML file, kept in its own repository. Everything else, such as sessions, receipts and statements, the system derives from them.

The organization

Who the operator is: the name, numbers and address on every receipt, and how drivers pay.

organization.yaml
manifest: cpms/v1
kind: organization
organization:
  key: example
  name: Example Charging AB
  org_number: "000000-0000"
  vat_number: SE000000000001
  address: "Exempelgatan 1, 123 45 Exempelstad"
  email: info@example.com
  payment_mode: direct            # drivers pay into your own Stripe account
  currency: SEK
  display_timezone: Europe/Stockholm
  operator_id: "SE*EXA"           # eMI3: your OCPI party and the prefix of every EVSE id
  • key is the stable name that the other documents use. Every object has one, so renaming something never breaks a reference.
  • payment_mode: direct means that drivers pay into the operator's own Stripe account, so the operator is the seller.
  • operator_id is needed once a site is published over OCPI; every EVSE id is derived from it.

The price list

One for the whole organization. A tariff is the price per kWh, a plan is the offer, and a channel says which plan a driver gets.

pricelist.yaml
manifest: cpms/v1
kind: pricelist
organization: example

tariffs:
  - key: standard
    name: Standard
    kind: flat
    amounts: incl_vat             # the price incl. VAT is exact
    vat_rate: "0.25"
    prices:                       # dated rows, oldest first
      - {from: 2026-01-01, per_kwh: "4.50"}
      - {from: 2026-12-01, per_kwh: "4.70"}

plans:
  - key: dropin
    name: Drop-in
    tariff: standard
    requires: none                # anyone who scans the QR code
    preauth_amount: "300"         # reserved on the card, incl. VAT
  - key: roaming
    tariff: standard              # roaming drivers pay the same per kWh
    requires: roaming

channels:
  adhoc: [dropin]                 # what a driver without an account gets
  roaming: {default: roaming}     # drivers from roaming partners
  • amounts: incl_vat says which side of the price is exact. The other side is calculated, so 4.70 incl. VAT stays 4.70 on the receipt.
  • Prices are dated rows. A price change is a new row with its date, and a row that has taken effect cannot be edited, so old receipts stay explainable.
  • Drop-in and roaming share one tariff, so they pay the same and a price change is one line.
  • Amounts are quoted strings, never floats. A float is not money.

A site

One file per station, with what the installer knows and nothing commercial: a site never names a price.

exempelgatan.yaml
manifest: cpms/v1
kind: site
organization: example
site:
  key: exempelgatan
  name: Exempelgatan 12
  address: Exempelgatan 12
  city: Exempelstad
  postal_code: "123 45"
  coordinates: {latitude: "59.0000000", longitude: "18.0000000"}
  publish: true                   # listed in OCPI, e.g. Nobil
  fuse_groups:
    - key: main
      name: Main fuse
      physical_fuse_amps: 63
      budget_amps: 50             # the balancer stays within this
      charge_points:
        - key: cp1
          ocpp_id: SIM-0001
          phase_rotation: [L1, L2, L3]
          outlets:
            - {connector: 1, key: cp1-1, plug: type2_socket, phases: 3, max_amps: 32}
            - {connector: 2, key: cp1-2, plug: type2_socket, phases: 3, max_amps: 32}
        - key: cp2
          ocpp_id: SIM-0002
          phase_rotation: [L2, L3, L1]   # wired one phase over
          outlets:
            - {connector: 1, key: cp2-1, plug: type2_socket, phases: 3, max_amps: 32}
            - {connector: 2, key: cp2-2, public: false}   # not offered to drivers
        - key: cp3
          ocpp_id: SIM-0003
          phase_rotation: [L3, L1, L2]
          outlets:
            - {connector: 1, key: cp3-1, plug: type2_cable, phases: 3, max_amps: 16}
  • budget_amps is what the load balancer may use. It can be set below physical_fuse_amps, for example for a smaller grid subscription, but never above it.
  • phase_rotation is set per charge point. Boxes are often wired with the phases rotated so that single-phase cars spread over L1–L3, and the balancer needs to know which station phase each box draws from.
  • publish: true lists the site over OCPI, for example in Nobil. A published site must then give its address, coordinates and, for each public outlet, plug, phases and max_amps.
  • An outlet with public: false is not offered to drivers. The files contain no secrets: charge point keys are generated by the server.

How a change goes live

The same two commands work for all three kinds of file, as with infrastructure-as-code.

  • cpms manifest plan site.yaml shows what would change and any warnings, such as sessions that are running now. It checks the file against the organization's other documents: a budget above the fuse, a plan naming a missing tariff, or an edited price row is an error.
  • cpms manifest apply applies exactly what was planned, with a message saying why. If the document changed on the server since the plan, it refuses, and you plan again.
  • Nothing is removed unless you pass --prune.
  • Every applied version is kept with its message and author: cpms manifest history lists them.

The command line is on PyPI as leancpms-cli:

uv tool install leancpms-cli    # or: pipx install leancpms-cli
cpms login --url https://cpms.example.com

cpms login opens your browser, where you log in to the portal and approve the key.

There is a JSON Schema for the files, so editors can validate and autocomplete them as you type. It comes with the command line: cpms manifest schema. cpms manifest validate checks a file offline.

An example, shortened: raising the budget and adding a charge point
$ cpms manifest plan exempelgatan.yaml
example site exempelgatan: version 3
  ~ site.fuse_groups[main].budget_amps: 50 → 63
  + site.fuse_groups[main].charge_points[cp4]
  ! fuse group exempelgatan/main has 2 active sessions; new budget applies next cycle
  priced by price list version 2: 6 public outlets → plan dropin, 4.50 SEK/kWh incl. VAT

$ cpms manifest apply exempelgatan.yaml -m "New grid subscription, one more box"
  …
apply? [y/N] y
applied example site exempelgatan version 4

$ cpms manifest history example --site exempelgatan
VERSION  APPLIED (UTC)        AUTHOR            MESSAGE
v1       2026-03-02 08:41:10  key:lcp_4f2a:cli  First three boxes
v2       2026-05-19 13:05:47  key:lcp_4f2a:cli  cp2 is wired L2-L3-L1
v3       2026-08-11 10:22:03  key:lcp_4f2a:cli  Outlet cp2-2 reserved for the property
v4       2026-11-02 09:14:31  key:lcp_4f2a:cli  New grid subscription, one more box

Your files, your repository

It works the same whether you self-host or use LeanCPMS Cloud. Keep the files in your own git repository, review changes as pull requests, or let an AI agent propose them.