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}")

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)

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

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.1.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.1.0-py3-none-any.whl (17.6 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: pypinergy-1.1.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.1.0.tar.gz
Algorithm Hash digest
SHA256 f15517ea7db7cea72ba8ffa69111bb5188d6a9e376d7c57cbf33a6bbc7f6e066
MD5 c56c70e5dca8e96472dfe09438e18b9f
BLAKE2b-256 050b90940586db7e2fc498c40a05c4a86f9eb674c5177b09047e9419e7502bc9

See more details on using hashes here.

File details

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

File metadata

  • Download URL: pypinergy-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 17.6 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.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 31983e168bd838aa43170cd0a6b5d22e3a32ba1f6f417d3ff29a15d10e407904
MD5 95b0ef60d04e660230b05d4c28f9c273
BLAKE2b-256 6ead418600bcb1c461108b01e6dac39d9aed94ff87381779d366681d8dba5fe5

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