Skip to main content

Python client library for the Pinergy smart meter API

Project description

PyPinergy Logo

PyPI version Python versions CI Status Coverage Ruff Documentation License: MIT

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: False means 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pypinergy-1.0.0.tar.gz (1.2 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pypinergy-1.0.0-py3-none-any.whl (16.5 kB view details)

Uploaded Python 3

File details

Details for the file pypinergy-1.0.0.tar.gz.

File metadata

  • Download URL: pypinergy-1.0.0.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

Hashes for pypinergy-1.0.0.tar.gz
Algorithm Hash digest
SHA256 000a0c7f8c2309d215b5e689dd74faea4016765c5c1782595cd59e8726316bb2
MD5 15b4a51d2f425832e00d0968d294cc66
BLAKE2b-256 a482682af795abffbe40b0b70518fbb6a248857e566108a4fa49e132b43e18f5

See more details on using hashes here.

File details

Details for the file pypinergy-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: pypinergy-1.0.0-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

Hashes for pypinergy-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 716cd9da585bbf901df61580fe57421f1a99743a0486fb9fd40c8b7a8e1dfb62
MD5 ab060273530d7114f52e408c59145a63
BLAKE2b-256 062b737d798f6b1311ad2ae685d8127e3ecc8e5c96f834e7d7aad83c2f87b193

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page