Python client library for the Pinergy smart meter API
Project description
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}")
Notifications
notif = client.get_notification_preferences()
print(f"Email notifications: {notif.email}")
print(f"SMS notifications: {notif.sms}")
print(f"Phone notifications: {notif.phone}")
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)
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
Unit tests (no credentials required)
pip install -e ".[dev]"
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pypinergy-0.1.8.tar.gz.
File metadata
- Download URL: pypinergy-0.1.8.tar.gz
- Upload date:
- Size: 1.2 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
960019fe382182ffb0579d479455f884aaacbc2e828576142ffd043bb2536de7
|
|
| MD5 |
18ed7763a3becc0c36357452509501e5
|
|
| BLAKE2b-256 |
6e938e52df96cf299a1aa92172a41381645a6f594dd9729d9e610a1c0573a363
|
File details
Details for the file pypinergy-0.1.8-py3-none-any.whl.
File metadata
- Download URL: pypinergy-0.1.8-py3-none-any.whl
- Upload date:
- Size: 16.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eadd10ee1279de1f3762b8c595faa7775bcc58f5e3b8db8cc1c99ebd817d13c0
|
|
| MD5 |
2cc4166b182aa16f8367846c80b6b2ee
|
|
| BLAKE2b-256 |
bda49ee2637aeb07b20f78c3519dabe50a13b22deddc3d1d267f58d4d80be1e5
|