Skip to main content

enedis-client

Fetches the load curve of a French Linky meter through the MyElectricalData gateway, and exports it as JSON and CSV.

It is also the foundation of a Home Assistant integration: all the data-access logic lives here, so it can be reused as-is.

Getting started

make setup      # venv and dependencies
make demo       # try everything, with no token and no network

make demo runs the whole chain — window splitting, parsing, aggregation, export — on simulated data. It is the fastest way to see what the tool produces without spending any of your Enedis quota.

For real data:

cp .env.example .env    # then fill in ENEDIS_PDL and ENEDIS_TOKEN
make check              # verify the consent and the remaining quota
make daily              # daily consumption in kWh -> dumps/
make fetch              # load curve (needs hourly collection enabled)

Start with make daily. Daily consumption depends only on the consent and works immediately. The load curve additionally requires hourly collection to be enabled on the Enedis side — see Troubleshooting.

Commands

Command Effect
enedis check Validates .env, the Enedis consent state and the call quota
enedis daily --days 30 Daily consumption in kWh — works without hourly collection
enedis fetch --days 30 Load curve at the meter's own step
enedis fetch --start 2026-01-01 --end 2026-02-01 A precise period (--end is exclusive)
enedis demo --days 20 The load curve on simulated data
enedis cache-clear Erases the data the gateway has cached

Shared options: --format json|csv|both, --out <directory>, --no-cache, -y/--yes, -v for detail, --env-file for an alternative .env.

--end is exclusive: --end 2026-02-01 stops at 31 January inclusive. Impossible periods (reversed, in the future, beyond 24 months) are rejected with a message and exit code 2, before any network call.

The binary is called enedis once the project is installed; python -m enedis_client works too.

The cache stores your data with a third party

The /cache endpoints have your data stored encrypted, for 30 days, on MyElectricalData's servers. It is a trade-off: the cache spares the Enedis quota, which is tight.

The cache is on by default, and every command says so on screen. --no-cache goes straight to Enedis without leaving anything behind, at the cost of one quota call. enedis cache-clear erases what is already there.

What the client covers

Endpoint Method Data
consumption_load_curve consumption_load_curve() Average power in W, at the meter's step
production_load_curve production_load_curve() The same, for solar production
daily_consumption daily_consumption() Energy in Wh, per day
daily_production daily_production() The same, for production
contracts contracts() Subscribed power, tariff option
valid_access valid_access() Consent, quota, reset time
*/cache (DELETE) clear_cache() Purge of the gateway cache

identity, contact and addresses also exist on the gateway but are not exposed: they are personal details unrelated to consumption.

As a library

from datetime import date, timedelta
from enedis_client import MyElectricalDataClient, Settings, fetch_daily, fetch_load_curve

today = date.today()
with MyElectricalDataClient(Settings.from_env()) as client:
    # Daily: one energy in Wh per day.
    series = fetch_daily(client, today - timedelta(days=30), today)
    print(f"{series.total_energy_kwh:.1f} kWh over {len(series)} days")

    # Load curve: one average power in W per interval.
    curve = fetch_load_curve(client, today - timedelta(days=7), today)
    for reading in curve.readings[:3]:
        print(reading.start, "->", reading.end, reading.power_w, "W", reading.energy_wh, "Wh")

An AsyncMyElectricalDataClient mirrors the same surface on httpx.AsyncClient, for callers that must not block an event loop — Home Assistant, for instance.

DailySeries and LoadCurve are deliberately distinct types: the daily series carries Wh of energy, the load curve W of power. Feeding one to the other's parser raises an explicit ParseError rather than producing numbers that are wrong by a factor of two.

What is tricky about the Enedis format

Three details silently break the arithmetic if you miss them. They are handled in models.py and covered by tests.

  • date is the end of the interval, not its start. IntervalReading exposes both (start and end).
  • value is an average power in watts, not an energy. energy_wh does the conversion using the interval's real duration.
  • The measurement step is not always 30 minutes: Enedis delivers PT10M, PT30M or PT60M depending on the meter — and it has been observed changing in the middle of a day. It is read from every single measurement.

On top of that, the clock change: in October the hour 02:00–03:00 happens twice and Enedis returns the same timestamps twice. They are disambiguated by arrival order. To compare or subtract two readings, use end_utc — between two datetime objects sharing a time zone, Python subtracts the naive values and ignores the offset.

API limits

  • Load curve: 7 days maximum per call, 24 months of history.
  • Daily: 1095 days per call, 36 months of history. Enedis says so itself — "Il est possible de recevoir au maximun 1095j de données consécutives".
  • Nothing for the current day: bounds are clamped automatically into the servable window, and fetch/daily split and stitch longer periods.
  • A long range is announced before it is run. The number of calls is printed up front, and beyond 20 the command asks for confirmation — two years of history is 105 calls against a quota of roughly 50 per day. --yes overrides.
  • Two quotas, not one. enedis check shows the gateway counter (calls / quota), but Enedis additionally applies its own throttling, which can refuse a call while that counter still reads zero. The response then carries the reopening time, and enedis fetch relays it verbatim. That refusal is never retried: access does not reopen for hours.
  • The gateway cache is used by default; --no-cache forces a real Enedis call.

Troubleshooting

Message Cause What to do
no measurement between … hourly collection must be enabled The gateway answers 404 no measure found. The consent can be valid without hourly collection being enabled: it is a separate option, switched on in the Enedis customer account (Je souhaite accéder à mes données horaires). Enable it, then wait: it goes through En cours d'activation and the first readings arrive within a few days. In the meantime, enedis daily works.
Enedis call quota reached … Access reopens at … Enedis throttling, independent of the counter shown by enedis check. Wait for the stated time. The gateway cache avoids coming back.
token rejected by the gateway (403 Le point de livraison ne correspond au token fourni) ENEDIS_TOKEN invalid, or tied to another delivery point. Renewing the consent invalidates the previous token. Take the token shown on myelectricaldata.fr and put it back into .env.
Enedis consent missing or expired The consent has an end date. Renew it on myelectricaldata.fr.

enedis check answers the last three cases without spending any Enedis quota.

Home Assistant integration

The custom_components/enedis_med/ directory holds an integration installable through HACS as a custom repository (category Integration), then added from Settings → Devices and services.

Configuration is entirely through the UI: delivery point and token. The pair is validated against the gateway before the entry is created, so a typo shows up immediately.

Consumption appears in the Energy dashboard under Grid consumption, named Enedis <pdl> consommation. It is not an entity but an external statistic: that is the only way to inject past history, which a plain sensor cannot do.

What you should know

  • The token is stored in clear text in .storage/core.config_entries. That is how Home Assistant works for every integration.
  • Renewing a consent on myelectricaldata.fr invalidates the previous token. Home Assistant detects this and opens a re-authentication form that asks only for the token.
  • Catch-up is spread out. One call covers 7 days and the quota is around 50 per day: after a long outage the dashboard fills over several cycles rather than in one go. The cycle is hourly, and it makes no API call at all while yesterday is already complete.
  • Impossible measurements are discarded. Any interval above the subscribed power read from your contract is skipped and logged. On real data, a point at 20,822 W against a 9 kVA subscription took the total error from 5.75 % down to 1.31 % against the daily index readings.

Development

make test    # no network calls: respx simulates the gateway
make lint    # ruff style check

The environment requires Python 3.14, imposed by pytest-homeassistant-custom-component. make setup installs it through uv.

To freeze the exact format your own meter returns:

python scripts/capture_fixture.py --day 2026-09-01

One call, on one day; the delivery point is replaced and the power values are jittered, so the resulting fixture is safe to commit.

Publishing

The GitLab CI runs on the self-hosted COMPX runner (tag roims-tech).

Trigger Version produced Destination
Push to develop <next patch>.dev<pipeline> The project's GitLab package registry
Tag 1.2.3 or v1.2.3 1.2.3 Public PyPI

Development builds are pre-releases under PEP 440: pip ignores them unless --pre is given, so they cannot supersede the stable release. A naive <version>-<build> would normalise to <version>.post<build>, which would instead be installed by default.

Publishing a stable release takes two steps: align version in pyproject.toml, then push the matching tag. If the two disagree, the build job fails before anything is published and the pypi job is skipped.

pip install --index-url https://gitlab.com/api/v4/projects/86382035/packages/pypi/simple \
            "enedis-client==1.0.1.dev2"

Deploying the Home Assistant integration

HACS installs from GitHub only, so this GitLab repository cannot be added as a HACS custom repository. The integration is deployed by copying custom_components/enedis_med/ into Home Assistant's /config/custom_components/.

The pipeline carries a manual home-assistant job, available on tags only. It runs on the COMPX runner, which sits on the same network as the Home Assistant VM, so it reaches it directly. It needs two CI variables:

Variable Content
HA_SSH_KEY_B64 SSH private key, base64-encoded on a single line, masked and protected
HA_HOST Instance address (optional, defaults to the value in .gitlab-ci.yml)

The key is base64-encoded because GitLab cannot mask a multi-line value. The variable must be protected so that only protected tags and branches can read it — otherwise any pipeline, including one from a merge request, could use a key that grants root access to Home Assistant.

Deployment is deliberately manual: it touches a live system and must never fire on its own. It is offered on tags and on main — the latter is handy for shipping an integration fix without cutting a release.

Before copying anything, the job checks that the version pinned by manifest.json is actually published on PyPI. Home Assistant installs a custom integration's requirements from PyPI when it loads it, so deploying a manifest that pins an unpublished version yields a broken integration and an obscure log entry. The check retries, because the PyPI index lags a few seconds behind an upload.

Disk footprint

The pipeline is deliberately frugal: python:3.14-alpine image (82 MB on disk against 179 for slim), shallow clone at 20 commits, uv cache keyed on the hash of pyproject.toml, artifacts limited to dist/ (77 kB) and expiring in a day, superseded pipelines cancelled automatically.

Above all: the jobs that gate publication install only the 34 MB of library dependencies, not the 737 MB the Home Assistant test harness requires — it is the library that gets published, not the integration. The tests under tests/ha/ still run locally through make test.

Structure

File Role
config.py Reading and validating .env, masking the token
client.py HTTP calls, retries, error translation
async_client.py The same surface, asynchronous
http.py Shared mapping from HTTP status to exception
models.py Parsing, time zone, energy; LoadCurve and DailySeries
hourly.py Hourly aggregation and plausibility filter
windows.py Window splitting (7 days hourly, 1095 daily)
loadcurve.py Chaining windows and stitching them back
export.py JSON and CSV writing
errors.py One exception per actionable cause
cli.py The check, daily, fetch, demo, cache-clear commands
demo.py Fake gateway backing demo mode

Metadata

Release files for enedis-client 1.0.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for enedis-client 1.0.2
File Size Uploaded Provenance
enedis_client-1.0.2.tar.gz 47.0 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for enedis-client 1.0.2
File Interpreter ABI Platform
enedis_client-1.0.2-py3-none-any.whl Python 3 none any Details

Total release size: 82.7 kB

Release files / enedis_client-1.0.2.tar.gz

Download URL enedis_client-1.0.2.tar.gz
Size 47.0 kB
Tags Source
SHA-256 checksum
How to use checksums
300cf27f046ad97ccc9f57b2c5a6178d0da761ed9a88d213c7f3a02e1c9fc98f
BLAKE2b-256 checksum
How to use checksums
505bac0831c9f737c53d5d9c856358aed9e3067b1d1924470d22024628c49412
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release files / enedis_client-1.0.2-py3-none-any.whl

Download URL enedis_client-1.0.2-py3-none-any.whl
Size 35.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0a015c345a60cff4637ed0b0338ebdaf90a63f75042610c3b2ea99f9c49f7e89
BLAKE2b-256 checksum
How to use checksums
efe28aefef48250a02e3b0c5ade334e1cbeef04a22376d9acd902d5d9f2b26b5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Security for enedis-client 1.0.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page