This application aggregates chargepoint data from different sources and provices it without authentication using an OCPI-style JSON-API and a vector tile server.
Mobilithek sources were last validated on 2026-10-28 with dev/mobilithek_helper/validate_datex2.py. DATEX II data
has to be valid against the DATEX II schema, otherwise the import fails as a whole. See
Mobilithek helper scripts for details. Problems of dysfunctional sources were re-checked on
2026-11-01 and reported at datex2-publications-issues.
| name | uid | realtime | credentials | comment |
|---|---|---|---|---|
| Bundesnetzagentur: API | bnetza_api | false | false | Additional config ignore_operators:: list[str] is supported, which will ignore given operators during import. Set to weekly download, as it does not change so often. |
| Bundesnetzagentur: Excel | bnetza_excel | false | false | |
| chargecloud: Stadtwerke Ludwigsburg | chargecloud_ludwigsburg | true | true | |
| chargecloud: Stadtwerke Pforzheim | chargecloud_pforzheim | true | false | |
| chargecloud: Stadtwerke Stuttgart | chargecloud_stuttgart | true | false | |
| chargecloud: Stadtwerke Tübingen | chargecloud_tuebingen | true | false | |
| Datex2: AH Beteiligungsgesellschaft | datex2_ah_beteiligung | true | true | |
| Datex2: Ampeco | datex2_ampeco | true | true | Static data is schema-invalid (messageContainer, service providers, operating hours), see #25 |
| Datex2: Audi charging hub | datex2_audi | true | true | Realtime data is empty, see #26 |
| Datex2: Chargecloud | datex2_chargecloud | true | true | |
| Datex2: e-clearing.net | datex2_e_clearing_net | true | true | |
| Datex2: EAAZE | datex2_eaaze | true | true | |
| Datex2: EcoMovement | datex2_ecomovement | true | true | |
| Datex2: Elu Mobility | datex2_elu_mobility | true | true | Static data is schema-invalid (pointLocation instead of locPointLocation), see #27 |
| Datex2: EnBW | datex2_enbw | true | true | |
| Datex2: ENERANDO | datex2_enerando | true | true | |
| Datex2: enio | datex2_enio | true | true | |
| Datex2: eRound | datex2_eround | true | true | |
| Datex2: Eulektro | datex2_eulektro | true | true | |
| Datex2: EV Price | datex2_ev_price | true | true | |
| Datex2: EWE | datex2_ewe | true | true | |
| Datex2: Flavia | datex2_flavia | true | true | |
| Datex2: GLS Mobility | datex2_giroe | true | true | Realtime data is a delta pull and was empty on 2026-10-29, so EVSE matching is unverified |
| Datex2: GP JOULE Connect | datex2_gp_joule | true | true | |
| Datex2: Grid&Co | datex2_gridandco | true | true | Static data is schema-invalid (external identifier without identifier), see #28 |
| Datex2: Inergia | datex2_inergia | true | true | Via Mobilithek |
| Datex2: Inselwerke | datex2_inselwerke | true | true | |
| Datex2: K.u.L. Rudnick | datex2_kul_rudnick | false | true | No data published, see #29 |
| Datex2: ladebusiness | datex2_ladebusiness | true | true | Data offer was withdrawn |
| Datex2: Ladesonne | datex2_ladesonne | true | true | |
| Datex2: Lichtblick | datex2_lichtblick | true | true | |
| Datex2: Midorion | datex2_midorion | true | true | |
| Datex2: Monta | datex2_monta | true | true | Static data is schema-invalid (times without seconds and offset), see #30 |
| Datex2: msu | datex2_msu | true | true | |
| Datex2: Pump | datex2_pump | false | true | Static data is schema-invalid (versionG at energy rates), see #31 |
| Datex2: Qwello | datex2_qwello | true | true | |
| Datex2: R & M Beteiligungsgesellschaft | datex2_r_m_beteiligung | true | true | |
| Datex2: Road | datex2_road | true | true | |
| Datex2: Smatrics | datex2_smatrics | true | true | |
| Datex2: Sonnenstrom Saarpfalz | datex2_sonnenstrom_saarpfalz | true | true | |
| Datex2: Taubert Consulting | datex2_taubert | true | true | |
| Datex2: Tesla | datex2_tesla | true | true | |
| Datex2: Ulm | datex2_ulm | true | true | |
| Datex2: Vaylens | datex2_vaylens | true | true | Static data is schema-invalid (times without seconds and offset), see #32 |
| Datex2: Virta | datex2_virta | true | true | |
| Datex2: Volkswagen Charging Group | datex2_volkswagen | true | true | Realtime data is outdated since 2026-10-02 and does not match static data, see #33 |
| Datex2: Wirelane | datex2_wirelane | true | true | |
| eliso | eliso | true | true | Custom JSON format instead of DATEX II, see #34 |
| Giro-e | giroe | true | true | |
| Heilbronn Heckarbogen | heilbronn_neckarbogen | true | true | |
| Lichtblick | lichtblick | true | true | Currently dysfunctional |
| OCHP: Albwerk | ochp_albwerk | true | true | |
| OCHP: Ladenetz | ochp_ladenetz | true | true | |
| OCPI: Stadtnavi | ocpi_stadtnavi | true | false | |
| OpenData Swiss | opendata_swiss | true | false | |
| PBW | eaaze_pbw | true | true |
dev/mobilithek_helper contains scripts to manage the Mobilithek subscriptions of the charging data sources. Managing
subscriptions needs a user login, so the scripts read MOBILITHEK_USER and MOBILITHEK_PASSWORD from .env.
python dev/mobilithek_helper/list_charging_offers.py # all charging data offers with subscription and import service
python dev/mobilithek_helper/list_subscriptions.py # all subscriptions of the account
python dev/mobilithek_helper/subscribe.py <data offer id> [<data offer id> ...]
python dev/mobilithek_helper/unsubscribe.py <subscription id> [<subscription id> ...]
python dev/mobilithek_helper/send_reminder.py [--dry-run] --to <email> <data offer id> [<data offer id> ...]send_reminder.py sends a German plain text email to the contact person of a provider, asking to approve pending
subscriptions. The Mobilithek does not expose contact addresses of other organisations, so the recipient has to be given
explicitly. It reads SMTP_HOST, SMTP_PORT (465 uses SSL, otherwise STARTTLS), SMTP_USER, SMTP_PASSWORD and
SMTP_FROM from .env.
validate_datex2.py validates the data of a subscription against the DATEX II v3.5 and v3.7 import services without
touching the database. It shows which base class fits, why an import fails and how many realtime EVSE statuses match
static EVSEs. It needs the webapp and the Mobilithek machine certificate in files/, so it runs in the flask container:
docker compose run --rm flask python dev/mobilithek_helper/validate_datex2.py <static subscription id> [<realtime subscription id>]There is a matching algorithm which matches live data sources with bnetza sources. You can find details at our matching docs.
At api.ocpdb.de you will find an OpenAPI documentation of public endpoints you can use.
The application provides a simple command line interface. You can access any cli command from within the container. The makefile provides a shortcut to run the cli:
make docker-run CMD="flask db upgrade"flask import allflask import static example_sourceflask import realtime example_sourceflask import images example_sourceflask source listflask source delete example_sourceflask match runflask location push-mobilithek-static # Full static DATEX II snapshot
flask location push-mobilithek-realtime-full # Full realtime status snapshot
flask location push-mobilithek-realtime-diff # Incremental realtime status diffSee DATEX II push to Mobilithek for details.
In addition to importing DATEX II data via Mobilithek (see the datex2_* data sources above), OCPDB can also
publish its aggregated dataset back to the Mobilithek in DATEX II format. Three kinds of
push are supported:
- Static snapshot – the full set of locations, charging stations, EVSEs, connectors, tariffs and operators.
- Realtime full snapshot – the current EVSE status of all locations (a
SNAPSHOT_PUSH). - Realtime diff – an incremental
DELTA_PUSHcontaining only EVSEs whose status changed since the last push. The last successful push timestamp is stored in Redis (last_datex_realtime_push); if nothing changed, no message is sent but the watermark is still advanced.
Both DATEX II 3.5 and 3.7 are supported, selected via MOBILITHEK_VERSION.
The pushes can be triggered manually through the CLI:
flask location push-mobilithek-static # Full static DATEX II snapshot
flask location push-mobilithek-realtime-full # Full realtime status snapshot (SNAPSHOT_PUSH)
flask location push-mobilithek-realtime-diff # Incremental realtime status diff (DELTA_PUSH, skipped when empty)When MOBILITHEK_ENABLED is true, the pushes are scheduled automatically as Celery periodic tasks. The schedule is
configurable in config.yaml:
MOBILITHEK_ENABLED: true
MOBILITHEK_VERSION: '3.5' # DATEX II version, '3.5' or '3.7'
MOBILITHEK_NAME: binary butterfly GmbH # supplier name sent in the exchange context
MOBILITHEK_STATIC_PUBLICATION_ID: 980054862909370368
MOBILITHEK_REALTIME_PUBLICATION_ID: 980084361378099200
MOBILITHEK_CERTIFICATE_FILENAME: 'mobilithek-binary-butterfly.crt.pem' # client certificate in KEY_DIR
MOBILITHEK_KEY_FILENAME: 'mobilithek-binary-butterfly.key.pem' # client key in KEY_DIR
# Push schedule (only active when MOBILITHEK_ENABLED is true):
MOBILITHEK_STATIC_PUSH_HOUR: 3 # static snapshot once a day at this hour ...
MOBILITHEK_STATIC_PUSH_MINUTE: 0 # ... and minute
MOBILITHEK_REALTIME_FULL_PUSH_FREQUENCY: 21600 # full realtime snapshot, in seconds (default every 6 hours)
MOBILITHEK_REALTIME_DIFF_PUSH_FREQUENCY: 60 # incremental realtime diff, in seconds (default every minute)Messages are sent via mutual-TLS POST to the Mobilithek publication endpoint, using the client certificate and key
located in KEY_DIR.
OCPDB extends the Location data model with a new field official_regional_code in non-strict mode. This field provides
the official regional code of a location, if available. Following regional codes are used:
- DEU: Regionalschlüssel
In Germany, we use the dataset Verwaltungsgebiete 1:25 000 (VG25) by Bundesamt für Kartographie und Geodäsie (BKG) for assigning official regional codes to locations, licenced as Creative Commons Namensnennung 4.0 International. We download the data using wget, transform the data using ogr2ogr and store it our Postgis database.
The script assumes that a once-only import is sufficient. You must delete the data/regionalschluessel/.vg25-imported "marker file" (and re-run the script) to trigger a re-import.
It also assumes that the data at the VG25 URL is immutable, the data will be downloaded only once. You must delete data/regionalschluessel/vg25.gpkg (and re-run the script) to trigger a re-download.
data/regionalschluessel/vg25.gpkg in order to trigger the download again, and data/regionalschluessel/.vg25-imported
to trigger the import again. You can use this mechanism for ansible automatization, too: if you drop the geopackage
at data/regionalschluessel/vg25.gpkg via ansible, you won't need to download the file during runtime.
You can run
flask location assign-regionalschluesselto assign regional codes to all locations already in the database. You can limit it to specific locations by providing the source id:
flask location assign-regionalschluessel --source-id 1In case of an Regionalschlüssel file update, make sure that the new geopackage has the same format as the old one. Afterwards, you can run
flask location assign-regionalschluessel --re-assignto re-assign regional codes to all locations.
The installation process is documented at INSTALL.md.
This project uses uv for dependency management. All runtime and
development dependencies are declared in pyproject.toml, and exact versions are pinned in
uv.lock. There are no requirements.txt files anymore.
- Runtime dependencies live in
[project.dependencies]. - Development / test dependencies live in the
devgroup under[dependency-groups]. - The shared
flask-openapiandbutterfly_pubsublibraries are served from binary butterfly's private package index, which is configured under[tool.uv.sources]/[[tool.uv.index]].
Common commands (all run inside the Docker containers in development, see the Makefile):
# Install all dependencies (incl. dev) into a virtual environment, respecting the lockfile
uv sync
# Install only runtime dependencies (used for the production image)
uv sync --no-dev
# Add / remove a dependency (updates pyproject.toml and uv.lock)
uv add some-package
uv remove some-package
# Upgrade the lockfile to the latest allowed versions
uv lock --upgrade
# Run a command inside the environment
uv run flask import allAfter changing dependencies, rebuild the dev image with make docker-build so the containers pick
up the new packages.
The application uses the logging module with some optional extensions. Logging can be configured using the config.yml.
The application provides some additional context and / or special output formats to log entries with custom formatters:
Most requests or tasks have context which can be used in log entries. The simplest is
LOGGING:
formatters:
human_readable:
(): webapp.common.logging.formatter.flask_attributes_formatter.FlaskAttributesFormatter
format: '%(asctime)s %(levelname)s %(source)s: %(message)s'
defaults: {'source': '-'}
handlers:
my_handler:
formatter: human_readableWith this example, you add the source to every log entry (if available). Please keep in mind that you need to
add a default, because not every log entry has a source_uid context.
Following additional log context variables are available:
sourceinitiatorlocationevseimage
You can output log entries in OpenTelemetry format, too:
LOGGING:
formatters:
open_telemetry:
(): webapp.common.logging.formatter.flask_open_telemetry_formatter.FlaskOpenTelemetryFormatter
prefix: ocpdb
service_name: OCPDB
handlers:
my_handler:
formatter: open_telemetryContext is automatically injected into log entry Attributes.
OCPDB is under AGPL. You will find details at the LICENCE.txt.
We appreciate bug reports and feature requests.