pyzonneplan
Asynchronous Python client for the Zonneplan API.
About
pyzonneplan is an async client for the Zonneplan API, focused on:
- Dynamic electricity and gas prices
- Electricity and gas consumption, also without a P1 meter
- Solar (PV) installation data
- Home battery monitoring and control modes
- EV charge point sessions and schedules
The library is under active development and endpoint coverage will keep expanding.
Installation
pip install pyzonneplan
Usage
Zonneplan authenticates with a one-time password (OTP) mailed to the account address instead of a password. Log in once, then store the token and pass it back in later:
import asyncio
import json
from datetime import date
from pathlib import Path
from pyzonneplan import Token, Zonneplan
TOKEN_FILE = Path("token.json")
async def main() -> None:
"""Log in (or restore the saved token) and read some data."""
token = Token.from_dict(json.loads(TOKEN_FILE.read_text())) if TOKEN_FILE.exists() else None
async with Zonneplan(email="you@example.com", token=token) as client:
if token is None:
challenge = await client.async_request_otp(source_name="pyzonneplan")
await client.async_submit_otp(challenge, input("One-time password: "))
account = await client.async_get_account()
for connection in account.connections:
print(connection.market_segment, connection.uuid)
electricity = next(c for c in account.connections if c.market_segment == "electricity")
summary = await client.async_get_summary(electricity.uuid)
print("Tariff group now:", summary.usage.type)
chart = await client.async_get_electricity_chart(electricity.uuid, date.today())
if chart.group is not None and chart.group.has_data:
print("Used today:", chart.group.delivered_kwh, "kWh")
# The client refreshes the token when it nears expiry, which rotates it: save the latest one.
if client.token is not None:
TOKEN_FILE.write_text(json.dumps(client.token.as_dict()))
if __name__ == "__main__":
asyncio.run(main())
async_logout() ends the session on the API and forgets the token, e.g. when the token isn't needed anymore.
Available data
| Method | Returns |
|---|---|
async_get_account() |
The account, its addresses, connections and contracts |
async_get_consumer_prices(chart, *, connection_uuid) |
Electricity (hourly or quarter-hourly) or daily gas prices, with price_at() and cheapest/most expensive helpers; with connection_uuid the prices of that connection's contract |
async_get_summary(connection_uuid) |
Live usage (P1 only), the current tariff group and a price forecast of about two days |
async_get_electricity_chart(connection_uuid, day, interval) |
Electricity used and returned per hour, day or month |
async_get_gas_chart(connection_uuid, day, interval) |
Gas used per hour, day or month |
async_get_electricity_consumption_insight(connection_uuid, start, end) / async_get_gas_consumption_insight(...) |
Energy used (and electricity returned) per month between two dates, with the average price per kWh or m³ |
async_get_electricity_delivered(connection_uuid) / async_get_gas(connection_uuid) |
P1 totals and live readings (meters), or None without a P1 meter |
async_get_electricity(connection_uuid) |
The same electricity data from the endpoint the current app uses, or None without a P1 meter |
async_get_p1_installation(contract_uuid) |
The P1 reader's meter identifiers, DSMR version and main fuse, and whether it is offline, or None without one |
async_get_electricity_home_consumption(connection_uuid) |
The home's electricity consumption, in the shape of async_get_electricity_delivered, or None without a P1 meter |
async_get_recent_power(connection_uuid, duration) |
The net power the P1 meter measured over the last 12, 24 or 48 hours (RecentPowerDuration); empty without a P1 meter |
async_get_pv_installation(connection_uuid) |
Every solar inverter on the connection and today's yield, or None without solar panels |
async_get_battery(connection_uuid, contract_uuid) |
Home battery state, results and modes |
async_get_battery_status(contract_uuid) |
A lighter home battery status: state of charge, power, state, control mode and whether its modem is online |
async_get_battery_chart(contract_uuid, day, interval) |
Home battery results per day or month |
async_get_battery_activity_chart(contract_uuid) |
A home battery's recent state of charge and what steered it; with day-ahead arbitrage also the planned charging and the market prices |
async_get_battery_asset_costs_chart(contract_uuid, start, end, interval) / async_get_battery_cumulative_asset_costs_chart(contract_uuid) |
Home battery costs (negative = earned, see earnings_euro) per day or month, or as a running total, as the app's history screen shows them |
async_get_battery_self_sufficiency_chart(connection_uuid, year) / async_get_battery_self_sufficiency_stats(connection_uuid, year) |
How much solar power the home used itself and how much consumption it saved, per month of a year and on average |
async_get_battery_control_mode(contract_uuid) / async_get_battery_home_optimization(contract_uuid) |
The battery's control mode, and the charge and discharge power for home optimization with the range each accepts |
async_get_battery_day_ahead_arbitrage(contract_uuid) |
The price spread (EUR) and market time resolution day-ahead arbitrage trades with |
async_get_charge_point(connection_uuid, contract_uuid) |
Charge point state, schedules and vehicles |
async_get_charge_point_control_mode(contract_uuid) |
Which ChargePointModes the charge point supports and which are enabled |
async_get_charge_point_charge_on_solar(contract_uuid) |
The charge on solar settings: surplus threshold (W), minimum charge time and allowed local hours |
async_set_locale(locale) |
Sets the language of the API's texts, e.g. nl-NL |
async_get_app_settings() / async_set_price_resolution(resolution) |
The settings the Zonneplan app syncs, and sets the PriceResolution the app shows prices in |
The contract UUIDs come from the account, e.g. connection.contracts_of_type(ContractType.HOME_BATTERY)
(constants and enums such as ContractType, ChartInterval, BatteryMode, BatteryState, TariffGroup and ChargePointStatus live in pyzonneplan.const; an enum field is None when the API sends a value the library doesn't know yet).
For a contract the account doesn't have, the battery and charge point methods raise ZonneplanNotFoundError.
Fields keep the API's raw units (1e-7 EUR, Wh, dm³, permille); properties such as delivered_kwh,
electricity_price_euro and state_of_charge_percent convert them.
Usage without a P1 meter arrives a day or more late from the grid operator: check has_data before trusting zeros,
and expect the last hours of a day to be revised when the next day arrives.
The PV, battery and charge point models follow the responses other projects have seen, but haven't been tested against live hardware yet. Reports and captured (anonymised) responses are welcome.
Actions
| Method | Does |
|---|---|
async_enable_battery_self_consumption(connection_uuid, contract_uuid) / async_disable_... |
Turns self consumption mode on or off |
async_enable_battery_home_optimization(connection_uuid, contract_uuid, *, max_charge_power_w, max_discharge_power_w) / async_disable_... |
Turns home optimization mode on (with new power limits, or the current ones) or off |
async_set_battery_backup_reserve(connection_uuid, contract_uuid, reserved_wh) |
Reserves energy for backup power |
async_enable_battery_day_ahead_arbitrage(contract_uuid, *, price_spread, market_time_resolution) |
Turns day-ahead arbitrage on: the battery trades when day-ahead prices differ at least price_spread (EUR) |
async_set_battery_control_mode(connection_uuid, contract_uuid, mode) |
Switches to a BatteryMode, turning the other modes off the way the app does |
async_start_charge_point_boost(connection_uuid, contract_uuid) / async_stop_charge_point(...) |
Starts charging now at full power, or stops charging |
async_stop_charge_point_boost(connection_uuid, contract_uuid) |
Stops a boost |
async_enable_charge_point_plug_and_charge(connection_uuid, contract_uuid) / async_disable_... |
Turns charging at full power on plug-in on or off |
async_enable_charge_point_always_flex(connection_uuid, contract_uuid) / async_disable_... |
Turns charging automatically on cheap prices on or off |
async_set_charge_point_start_mode(connection_uuid, contract_uuid, mode) |
Switches to a ChargePointStartMode, sending the action the app sends from the current mode |
async_resume_charge_point_auto_charging(connection_uuid, contract_uuid) |
Lets the charge point charge on cheap prices again after a manual stop |
async_start_charge_point_dynamic_session(connection_uuid, contract_uuid, end, *, kilometers or percentage, vehicle_uuid) |
Charges an amount by end at the cheapest prices |
async_set_charge_point_schedule(connection_uuid, contract_uuid, schedule, *, vehicle_uuid) |
Replaces the planned charging with ChargeSchedule windows; ended windows are dropped |
async_reset_charge_point_schedule(connection_uuid, contract_uuid) |
Clears the planned charging |
async_enable_charge_point_charge_on_solar(contract_uuid, *, solar_surplus_threshold_w, min_charge_duration, allowed_window_start, allowed_window_end) / async_disable_... |
Turns charging on solar surplus on (omitted settings keep their current value) or off |
A battery with both modes off trades on the dynamic prices (BatteryMode.DYNAMIC_CHARGING). The API doesn't
switch the other mode off for you; async_set_battery_control_mode() does, like the Zonneplan app. Day-ahead
arbitrage has no disable call: any other battery action except the backup reserve ends it. The battery
and charge point confirm a change asynchronously; until then their state reports processing.
Actions are sent once and never retried.
Errors
Every error derives from ZonneplanError:
ZonneplanAuthenticationError: the token is invalid or expired (log in again);ZonneplanInvalidOtpErrorwhen the OTP is rejected.ZonneplanRateLimitError: HTTP 429;retry_afterholds the seconds to wait. It isn't retried.ZonneplanRequestError: the API rejected the request (HTTP 4xx), e.g. an unknown chart interval;ZonneplanNotFoundErrorfor a 404, e.g. a device the account doesn't have. Not retried.ZonneplanResponseError: the response isn't JSON or doesn't have the expected shape.ZonneplanConnectionError/ZonneplanTimeoutError: a network error, a timeout or an HTTP 5xx. GET requests are retried with backoff first (max_retries, default 3); actions are never retried.
More examples can be found in the examples directory.
Documentation
Project documentation and API reference: https://erwindouna.github.io/pyzonneplan/
Contributing
Contributions are welcome. Please open an issue or pull request.
For local development:
uv sync --all-groups && uv run prek install
Run checks:
uv run prek run --all-files
Run tests:
uv run pytest
License
MIT License
Copyright (c) 2026 Erwin Douna
Metadata
Release files for pyzonneplan 0.3.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pyzonneplan-0.3.2.tar.gz | 40.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyzonneplan-0.3.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 85.1 kB
Release files / pyzonneplan-0.3.2.tar.gz
| Download URL | pyzonneplan-0.3.2.tar.gz |
|---|---|
| Size | 40.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1a27f099fd20ba711d7fe5a05559f29d068d3b3cc377ce2fa3e3e57fc655563c
|
|
BLAKE2b-256 checksum How to use checksums |
6233cc3dd35ef85115eeb67092de25edc5bf14ce80a9c5337c13bfd834f7e156
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.
Transparency logRelease files / pyzonneplan-0.3.2-py3-none-any.whl
| Download URL | pyzonneplan-0.3.2-py3-none-any.whl |
|---|---|
| Size | 44.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
bd8c144fa3fd8439effdd6063d9dd4042a156b243e70ced28cca69bb8938a1c7
|
|
BLAKE2b-256 checksum How to use checksums |
2bdf919b67cb6fe2856eeba45326376090c3d8819bcaf0c9642a6d41f1343e44
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.
Transparency log