Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,22 @@ PUMPERLY_DEFAULT_COUNTRY=ES
# OpenChargeMap — EV charging data, free key from https://openchargemap.org
# PUMPERLY_OCM_API_KEY=

# Spain EV chargers (Mapa REVE) — the official registry every Spanish charge
# point operator must file into. Free key, request it at
# https://www.mapareve.es/api-contacto (keys expire after ~1 year).
# When set, Spain's EV data comes from REVE instead of OpenChargeMap. The
# existing OpenChargeMap rows for Spain stay put during the backfill (so the
# map is never empty) and are deleted once REVE reaches 95% of the registry.
# Note: the API allows only 5 requests/hour of 100 locations each, so the
# first full load of ~14.5k chargers takes about 37 hours at the default 4
# pages per run (~30 hours if you raise it to 5). It fills in the background
# — nothing to do but wait, and expect duplicate Spanish pins until it lands.
# PUMPERLY_REVE_API_KEY=

# Pages fetched per hourly run (default 4). Values above 5 are clamped to 5,
# the API's published hourly limit.
# PUMPERLY_REVE_PAGES_PER_RUN=4
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Denmark (FuelPrices.dk)
# FUELPRICES_DK_API_KEY=

Expand Down
91 changes: 91 additions & 0 deletions .github/workflows/reve-key-renewal.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: REVE API key renewal reminder

# The Mapa REVE API key that feeds Spain's EV charging data expires once a year.
# When it does, the REVE scraper starts failing and Spanish charger data quietly
# goes stale — nothing else breaks, so it would go unnoticed for a long time.
#
# This opens an issue a week before expiry with the renewal steps in it.
#
# Renewing? Change BOTH the `schedule` cron below and KEY_EXPIRY to the new
# date, so next year's reminder still lands a week early.
#
# Current key expires: 2027-08-21 → reminder fires 2027-08-14.

on:
schedule:
# 08:00 UTC on 14 August, every year (a week before the 21 August expiry).
- cron: "0 8 14 8 *"
workflow_dispatch:

permissions:
contents: read
issues: write

env:
KEY_EXPIRY: "2027-08-21"

jobs:
remind:
name: Open renewal issue
runs-on: ubuntu-latest
steps:
- name: Open (or update) the renewal issue
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail

TITLE="Renew the Mapa REVE API key (expires ${KEY_EXPIRY})"

# Don't stack a new issue on top of last year's if one is still open.
EXISTING=$(gh issue list --state open --search '"Renew the Mapa REVE API key" in:title' \
--json number --jq '.[0].number // empty')

BODY=$(cat <<'EOF'
The Mapa REVE API key behind Spain's EV charging data expires on __EXPIRY__.

Nothing breaks loudly when it lapses: the scraper logs an HTTP error and
Spanish charger data simply stops being refreshed. Renew before the date.

## How to renew

1. Request a new key at https://www.mapareve.es/api-contacto — it is free.
Say the use is non-commercial and name Pumperly. Approval arrives by
email, usually within a few days, with the key and its new expiry.
2. Update the running deployment's `PUMPERLY_REVE_API_KEY` and redeploy.
The key is an environment variable — it is never committed.
3. Confirm it works. A single request is enough:

```
curl -sS -o /dev/null -w '%{http_code}\n' \
-H "x-api-key: $NEW_KEY" \
'https://www.mapareve.es/api/external/v1/locations?limit=1&page=1'
```

`200` means good, `401`/`403` means the key is wrong.
4. Update `.github/workflows/reve-key-renewal.yml` — both the `schedule`
cron and `KEY_EXPIRY` — to a week before the new expiry date, so this
reminder keeps working.
5. Close this issue.

## Worth knowing

- The API allows **5 requests per hour**, 100 locations per page. A full
reload of the ~14.5k-location registry takes about 30 hours, so a lapsed
key is not instantly recoverable — the data refills gradually.
- Terms of use: non-commercial only, Red Eléctrica de España must be
credited as the source, and the data must not be altered or
misrepresented. The credit lives in the in-app legal modal.

_Opened automatically by `.github/workflows/reve-key-renewal.yml`._
EOF
)
BODY=${BODY//__EXPIRY__/$KEY_EXPIRY}

if [ -n "$EXISTING" ]; then
echo "Issue #$EXISTING is already open — adding a comment instead."
gh issue comment "$EXISTING" --body "$BODY"
else
gh issue create --title "$TITLE" --body "$BODY"
fi
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Pumperly combines route planning with real-time fuel prices and EV charging stat

- **Route planning** — Geocoding via [Photon](https://github.com/komoot/photon), routing via [Valhalla](https://github.com/valhalla/valhalla), with alternative routes
- **Real-time fuel prices** — From government open data APIs and community sources
- **EV charging stations** — Via [Open Charge Map](https://openchargemap.org) across all supported countries
- **EV charging stations** — Via [Open Charge Map](https://openchargemap.org) across all supported countries, and the official [Mapa REVE](https://www.mapareve.es) registry in Spain
- **Detour calculation** — Each station shows estimated detour time from your route
- **"Cheapest within N min"** — Slider filters stations by maximum detour, highlights the best deal
- **Corridor station list** — Sorted by position along route, with price deltas vs average
Expand Down Expand Up @@ -97,6 +97,9 @@ Pumperly combines route planning with real-time fuel prices and EV charging stat
| Source | Coverage | License |
|---|---|---|
| [Open Charge Map](https://openchargemap.org) | All supported countries + United States (EV-only) | ODbL |
| [Mapa REVE](https://www.mapareve.es) (Red Eléctrica de España) | Spain — official operator-reported registry | Non-commercial, attribution required |

Spain uses Mapa REVE when `PUMPERLY_REVE_API_KEY` is set: it is the registry every Spanish charge point operator files into, so it is authoritative where Open Charge Map is crowdsourced. The two overlap heavily, so Open Charge Map stops being scraped for Spain immediately, its existing Spanish rows stay visible while REVE backfills, and they are deleted once REVE reaches 95% of the registry. Expect duplicate Spanish pins until then.

### Map & routing

Expand Down Expand Up @@ -381,6 +384,7 @@ services:
# API keys — get your own:
# TANKERKOENIG_API_KEY: "" # https://creativecommons.tankerkoenig.de
# PUMPERLY_OCM_API_KEY: "" # https://openchargemap.org (free, for EV data)
# PUMPERLY_REVE_API_KEY: "" # https://www.mapareve.es/api-contacto (free, Spain EV)
# FUELPRICES_DK_API_KEY: "" # Denmark fuel prices
ports:
- "3000:3000"
Expand Down Expand Up @@ -420,6 +424,7 @@ Some data sources require API keys (all free):
|---|---|---|
| `TANKERKOENIG_API_KEY` | Germany fuel prices | Register at [creativecommons.tankerkoenig.de](https://creativecommons.tankerkoenig.de) |
| `PUMPERLY_OCM_API_KEY` | EV charging stations | Register at [openchargemap.org](https://openchargemap.org), go to My Profile > My API Keys |
| `PUMPERLY_REVE_API_KEY` | Spain EV chargers (official registry) | Request at [mapareve.es/api-contacto](https://www.mapareve.es/api-contacto). Keys expire after about a year. The API allows 5 requests/hour, so the first full load takes about 37 hours at the default 4 pages per run, and fills in the background. |
| `FUELPRICES_DK_API_KEY` | Denmark fuel prices | Contact [fuelprices.dk](https://fuelprices.dk) |

Most countries work without any API key — they use open government data.
Expand Down
1 change: 1 addition & 0 deletions src/components/nav/legal-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ function SourcesContent() {
<h3>EV charging data</h3>
<ul>
<li><strong>Open Charge Map</strong> &mdash; EV charging station locations across all supported countries. Community-maintained, Open Data Commons Open Database License (ODbL). <a href="https://openchargemap.org" target="_blank" rel="noopener noreferrer">openchargemap.org</a></li>
<li><strong>Mapa REVE</strong> &mdash; EV charging station locations in Spain. Source: Red El&eacute;ctrica de Espa&ntilde;a, S.A.U. Used non-commercially and reproduced without alteration. <a href="https://www.mapareve.es" target="_blank" rel="noopener noreferrer">mapareve.es</a></li>
</ul>

<h3>Map and routing</h3>
Expand Down
21 changes: 21 additions & 0 deletions src/instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ const DEFAULT_INTERVALS: Record<string, number> = {
EV_RS: 24, EV_FI: 24, EV_EE: 24, EV_LV: 24, EV_LT: 24, EV_BA: 24,
EV_MK: 24, EV_TR: 24, EV_MD: 24, EV_AU: 24, EV_AR: 24, EV_MX: 24,
EV_US: 24,
// Spain Mapa REVE — the API allows only 5 requests/hour, so this crawls a
// few pages at a time and must run hourly to get through the registry.
EV_ES_REVE: 1,
};

export async function register() {
Expand Down Expand Up @@ -100,6 +103,7 @@ export async function register() {
const { ArgentinaScraper } = await import("./scrapers/argentina");
const { MexicoScraper } = await import("./scrapers/mexico");
const { OCMScraper } = await import("./scrapers/ocm");
const { REVEScraper } = await import("./scrapers/reve");
const { StaticScraper } = await import("./scrapers/static");
const { STATIC_DATASETS } = await import("./scrapers/data");

Expand Down Expand Up @@ -180,6 +184,8 @@ export async function register() {
EV_MX: () => new OCMScraper("MX"),
// US has no national fuel-price API — EV-only coverage via OCM (#85)
EV_US: () => new OCMScraper("US"),
// Spain's official EV registry — supersedes EV_ES when a key is set (#121)
EV_ES_REVE: () => new REVEScraper(),
};

// Register community-contributed static datasets (see scrapers/data/README.md).
Expand Down Expand Up @@ -223,6 +229,21 @@ export async function register() {
: Object.keys(scraperFactories).filter((c) => !c.startsWith("EV_"));
}

// Spain: the official Mapa REVE registry supersedes OpenChargeMap whenever a
// key is configured. They must never both run — 92% of REVE locations sit
// within 50m of an existing OpenChargeMap row, so the map would double-pin
// every Spanish charger. REVE also retires the rows it replaces once its
// backfill is complete (see scrapers/reve.ts).
if (process.env.PUMPERLY_REVE_API_KEY && countries.includes("EV_ES")) {
countries = countries.filter((c) => c !== "EV_ES");
if (!countries.includes("EV_ES_REVE")) countries.push("EV_ES_REVE");
console.log(
"[scraper] Spain EV: using Mapa REVE (official registry) instead of OpenChargeMap",
);
} else {
countries = countries.filter((c) => c !== "EV_ES_REVE");
}

// Resolve per-country intervals
for (const code of countries) {
// Priority: PUMPERLY_SCRAPE_INTERVAL_XX > PUMPERLY_SCRAPE_INTERVAL_HOURS > DEFAULT_INTERVALS
Expand Down
3 changes: 3 additions & 0 deletions src/scrapers/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { AustraliaNSWScraper } from "./australia-nsw";
import { ArgentinaScraper } from "./argentina";
import { MexicoScraper } from "./mexico";
import { OCMScraper } from "./ocm";
import { REVEScraper } from "./reve";

// ---------------------------------------------------------------------------
// Scraper CLI
Expand Down Expand Up @@ -128,6 +129,8 @@ const SCRAPERS: Record<string, Array<() => BaseScraper>> = {
EV_MX: [() => new OCMScraper("MX")],
// US has no national fuel-price API — EV-only coverage via OCM (#85)
EV_US: [() => new OCMScraper("US")],
// Spain's official EV registry — supersedes EV_ES when a key is set (#121)
EV_ES_REVE: [() => new REVEScraper()],
};

function usage(): never {
Expand Down
Loading
Loading