A Python client library for the Pinergy smart-meter API.
Full Documentation: https://irishsmurf.github.io/pypinergy/
Note: This library is built on a reverse-engineered, unofficial API. It is not affiliated with or endorsed by Pinergy.
Installation
pip install pypinergy
Requires Python 3.9+ and requests.
Quick Start
from pypinergy import PinergyClient
with PinergyClient("you@example.com", "your-password") as client:
# Check your current balance
balance = client.get_balance()
print(f"Balance: €{balance.credit_balance:.2f}")
print(f"Estimated days remaining: {balance.top_up_in_days}")
# Today's usage
usage = client.get_usage()
today = usage.day[0]
print(f"{today.date:%Y-%m-%d} {today.kwh:.2f} kWh €{today.amount:.2f}")
Authentication is lazy — the client logs in automatically on the first API
call (thread-safe: concurrent first calls perform a single login). You can also
call .login() explicitly if you want the LoginResponse data (account
details, credit cards, etc.).
The context manager releases the pooled HTTP connection on exit. A plain
client = PinergyClient(...) works too — call client.close() when done.
Authentication
Automatic (recommended)
client = PinergyClient("you@example.com", "your-password")
# First API call triggers login transparently
balance = client.get_balance()
Explicit login
from pypinergy import PinergyClient
client = PinergyClient("you@example.com", "your-password")
login = client.login()
print(f"Welcome, {login.user.first_name}!")
print(f"Account type: {login.account_type}")
print(f"Premises: {login.premises_number}")
print(f"Level pay: {login.is_level_pay}")
Check if an email is registered
if client.check_email("someone@example.com"):
print("Account exists")
else:
print("No account found for that email")
Logout
client.logout()
print(client.is_authenticated) # False
Logout discards the stored credentials: any further API call raises
PinergyAuthError immediately (no automatic re-login). Create a new
PinergyClient to authenticate again.
Usage Data
Smart / PAYG customers
usage = client.get_usage()
print("--- Daily (last 7 days) ---")
for entry in usage.day:
print(f" {entry.date:%Y-%m-%d} {entry.kwh:6.2f} kWh €{entry.amount:.2f}")
print("--- Weekly (last 8 weeks) ---")
for entry in usage.week:
print(f" w/c {entry.date:%Y-%m-%d} {entry.kwh:7.2f} kWh €{entry.amount:.2f}")
print("--- Monthly (last 11 months) ---")
for entry in usage.month:
print(f" {entry.date:%Y-%m} {entry.kwh:8.2f} kWh €{entry.amount:.2f}")
Each UsageEntry has:
| Field | Type | Description |
|---|---|---|
available |
bool |
Whether data is available for this period |
amount |
float |
Cost in euros (€) |
kwh |
float |
Energy consumed in kilowatt-hours |
co2 |
float |
CO₂ in kg (0.0 for renewable supply) |
date |
datetime |
UTC datetime for the start of the period |
date_dt |
datetime |
Alias for date (*_dt naming convention) |
date_ts |
int |
Raw Unix timestamp |
Level Pay customers
lp = client.get_level_pay_usage()
print("Half-hourly labels:", lp.labels[:4]) # ["00:00", "00:30", ...]
print("Tariff flags:", lp.flags[:2]) # ["Standard", ...]
for day_val in lp.values:
print(f" {day_val.label}: {day_val.day_kwh}")
Balance
bal = client.get_balance()
print(f"Current balance: €{bal.credit_balance:.2f}")
print(f"Days remaining: {bal.top_up_in_days}")
print(f"Last top-up: €{bal.last_top_up_amount:.2f} on {bal.last_top_up_time:%Y-%m-%d}")
print(f"Last meter reading: {bal.last_reading:%Y-%m-%d %H:%M UTC}")
print(f"Credit low? {bal.credit_low}")
print(f"Emergency credit? {bal.emergency_credit}")
print(f"Power off? {bal.power_off}")
BalanceResponse fields:
| Field | Type | Description |
|---|---|---|
credit_balance |
float |
Credit balance in euros (€) |
top_up_in_days |
int |
Estimated days until credit runs out |
pending_top_up |
bool |
Whether a top-up is pending |
pending_top_up_by |
str |
Who initiated the pending top-up |
last_top_up_amount |
float |
Amount of last top-up (€) |
last_top_up_time |
datetime | None |
UTC datetime of last top-up |
last_top_up_dt |
datetime | None |
Alias for last_top_up_time |
last_top_up_ts |
int | None |
Raw Unix timestamp of last top-up |
last_reading |
datetime | None |
UTC datetime of last meter reading |
last_reading_dt |
datetime | None |
Alias for last_reading |
last_reading_ts |
int | None |
Raw Unix timestamp of last reading |
credit_low |
bool |
Balance below configured threshold |
emergency_credit |
bool |
On emergency credit |
power_off |
bool |
Supply disconnected |
Top-Ups
topups = client.get_active_topups()
for sched in topups.scheduled:
owner = "you" if sched.current_user else sched.customer
print(f" Day {sched.top_up_day:2d} of each month: €{sched.top_up_amount:.0f} ({owner})")
if topups.auto_top_ups:
print("Auto top-ups configured:", topups.auto_top_ups)
current_user: Falsemeans the scheduled top-up belongs to another resident on the same premises (e.g. in an apartment building).
Usage Comparison
Compare your home's usage against a cohort of similar homes:
cmp = client.compare_usage()
for label, period in [("Today", cmp.day), ("This week", cmp.week), ("This month", cmp.month)]:
if not period.available:
continue
diff_kwh = period.kwh.users_home - period.kwh.average_home
direction = "more" if diff_kwh > 0 else "less"
print(
f"{label}: {period.kwh.users_home:.1f} kWh "
f"({abs(diff_kwh):.1f} kWh {direction} than average)"
)
ComparePeriod has three metric groups — euro, kwh, co2 — each with
users_home and average_home floats.
Configuration
Valid top-up amounts and alert thresholds
cfg = client.get_config_info()
print("Top-up options:", cfg.top_up_amounts)
print("Low-balance thresholds:", cfg.thresholds)
House and heating type reference data
defaults = client.get_defaults_info()
for ht in defaults.house_types:
print(f" {ht.id}: {ht.name}")
for ht in defaults.heating_types:
print(f" {ht.id}: {ht.name}")
Instant Top-Up
Use top_up() to charge a saved payment card and add credit to your Pinergy account.
The cc_token is available on the CreditCard object returned during login.
Valid top-up amounts can be retrieved via get_config_info().
# Retrieve the CC token from the login response
login_resp = client.login()
cc_token = login_resp.credit_cards[0].cc_token
# Check valid top-up amounts
config = client.get_config_info()
print(f"Valid amounts: {config.topup_amounts}")
# Trigger a top-up
result = client.top_up(amount=20.0, cc_token=cc_token)
print(f"Transaction ID: {result.transaction_id}")
print(f"New balance: €{result.new_balance:.2f}")
print(f"Amount added: €{result.amount:.2f}")
Notifications
notif = client.get_notification_preferences()
print(f"Email notifications: {notif.email}")
print(f"SMS notifications: {notif.sms}")
print(f"Phone notifications: {notif.phone}")
# Update notification preferences
client.update_notification_preferences(sms=True, email=True, phone=False)
Device Token (FCM)
For headless / server-side usage you can pass an empty string or skip this entirely — it only affects push notifications on mobile devices.
client.update_device_token(
device_token="", # empty for headless use
device_type="android",
os_version="",
)
App Version
Unauthenticated endpoint — does not require a login:
version_info = client.get_version()
print(version_info)
Error Handling
from pypinergy import PinergyClient
from pypinergy.exceptions import (
PinergyAPIError,
PinergyAuthError,
PinergyHTTPError,
PinergyResponseError,
PinergyTimeoutError,
)
client = PinergyClient("you@example.com", "wrong-password")
try:
client.login()
except PinergyAuthError as e:
print(f"Bad credentials: {e}")
try:
balance = client.get_balance()
except PinergyAPIError as e:
print(f"API error {e.error_code}: {e.message}")
except PinergyTimeoutError as e:
print(f"Request timed out: {e}")
except PinergyHTTPError as e:
print(f"Network error: {e}")
except PinergyResponseError as e:
print(f"Unexpected response payload: {e}")
| Exception | When raised |
|---|---|
PinergyError |
Base class for all library errors |
PinergyAuthError |
Invalid credentials, expired/rejected token, or use after logout() |
PinergyAPIError |
API returned success: false (has .error_code and .message) |
PinergyHTTPError |
Network-level failure (5xx, DNS, connection errors, etc.) |
PinergyTimeoutError |
Request exceeded the configured timeout (subclass of PinergyHTTPError) |
PinergyResponseError |
Malformed or structurally invalid response payload (also subclasses ValueError) |
If the server rejects the auth token — whether as an HTTP 401 or as an
HTTP 200 success: false body with an auth-token message (e.g.
"Auth_token is not correct.") — the client drops the stale token before
raising PinergyAuthError, so the next call re-authenticates automatically.
Advanced Usage
Custom timeout
Applied to every network call; accepts a float for sub-second precision.
client = PinergyClient("you@example.com", "password", timeout=10)
Rate limiting
A built-in token-bucket rate limiter prevents accidental API abuse. Defaults: 2 requests/second sustained, burst of 5.
# Custom rate limit: 1 req/s, burst of 3
client = PinergyClient("you@example.com", "password", rate_limit=1.0, rate_burst=3)
Automatic retries
GET requests are automatically retried on transient failures (5xx, timeouts, connection errors) with jittered exponential backoff. POST requests are never retried to avoid duplicate side effects.
# Custom retry settings
client = PinergyClient(
"you@example.com", "password",
max_retries=3, # default: 2
retry_base_delay=1.0, # default: 0.5s
retry_max_delay=30.0, # default: 10s
)
Response size limit
Responses are checked against a size limit before JSON decoding to prevent memory exhaustion from malformed server responses.
# Custom limit (default: 2 MB)
client = PinergyClient("you@example.com", "password", max_response_bytes=5_242_880)
Response caching
Responses are cached in memory with per-endpoint TTLs (e.g. balance: 60s, config: 30 min). Cache hits bypass both the network and the rate limiter.
# Disable caching entirely
client = PinergyClient("you@example.com", "password", cache_disabled=True)
# Manual cache control
client.cache_flush() # clear all cached responses
client.cache_invalidate("/api/balance/") # invalidate a specific endpoint
Logout automatically flushes the cache to prevent stale authenticated data from leaking.
Custom base URL (testing / proxy)
client = PinergyClient("you@example.com", "password", base_url="http://localhost:8080")
Checking account type before fetching usage
login = client.login()
if login.is_level_pay:
data = client.get_level_pay_usage()
else:
data = client.get_usage()
Running Tests
Development Setup & Unit tests (no credentials required)
pip install -e ".[dev]"
pre-commit install
pytest tests/unit/ -v
Integration tests (real API)
export PINERGY_EMAIL=you@example.com
export PINERGY_PASSWORD=your-password
pytest tests/integration/ -v
All tests with coverage
pytest --cov=pypinergy --cov-report=html
Publishing to PyPI
Releases are automated via Trusted Publishing:
bump version in pyproject.toml and __version__ in
src/pypinergy/__init__.py (they must match), then push a matching tag:
git tag v0.1.6
git push origin v0.1.6
The publish.yml workflow verifies the tag
against the package version, builds the wheel and sdist, uploads to PyPI, and
creates a GitHub release.
License
MIT
Metadata
Release files for pypinergy 1.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pypinergy-1.2.0.tar.gz | 1.2 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pypinergy-1.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.2 MB
Release files / pypinergy-1.2.0.tar.gz
| Download URL | pypinergy-1.2.0.tar.gz |
|---|---|
| Size | 1.2 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c970eabb4e7afb97d4ada8267763ecf8460dad2c3f587db6091bacd9c6c532b8
|
|
BLAKE2b-256 checksum How to use checksums |
bd71aee9c890fc7e2924ce19eecb194b1313257dbad069b8149e67789f40036a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Release files / pypinergy-1.2.0-py3-none-any.whl
| Download URL | pypinergy-1.2.0-py3-none-any.whl |
|---|---|
| Size | 19.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
18c628229879b3b40fe38c0a5034f77cdb41718983d79fda4b44d03cd0cdeb84
|
|
BLAKE2b-256 checksum How to use checksums |
f26692de543b1dad0df0a02b7104038d4efa36b4726a5d8f7831e2615c3d3ad2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|