TariffKit
An offline electricity tariff engine for pricing, billing, and energy-system integrations. The first data provider supports PG&E residential rate plans under NEM 3.0 / the Net Billing Tariff (NBT); the package identity is deliberately not tied to one utility or tariff program.
Three schedules are vendored: E-ELEC (Electric Home), E-TOU-C (Time-of-Use, peak 4–9 p.m. every day), and EV2-A (Home Charging).
Under NBT your export credit is not a time-of-use schedule: it is an hourly Avoided Cost Calculator value that swings from about $0.06/kWh at midday to about $1.19/kWh on an August evening. Knowing what a kWh is worth right now, and what it will be worth over the next two days, is the input to every useful solar-and-battery dispatch decision.
from tariffkit import RateEngine
engine = RateEngine()
point = engine.price_now()
print(point.import_price.total) # $/kWh to draw from the grid
print(point.export_price.total) # $/kWh earned by exporting
print(point.spread) # positive => exporting beats self-consuming
curve = engine.forecast(hours=48)
for hour in curve.best_export_hours(3):
print(hour.start, hour.export_price.total)
Why it works offline
PG&E publishes 20 years of hourly export rates per vintage, as CPUC Resolution
E-5301 requires (roughly 40 MB of CSV per vintage). But that file is a lossless
expansion of a 576-cell matrix per year (12 months × 2 day types × 24 hours) per
component. tariffkit collapses it at build time, verifying losslessness cell by
cell, so the entire five-vintage dataset ships inside the wheel at 268 KiB
and every lookup is a few list indexes.
The retail side is similar: these schedules' period boundaries are identical every day
of the week including holidays and do not shift by season, so an import price is
fully determined by (season, hour).
Nothing here touches the network at runtime.
Documentation
| Configuration | Settings, CCA setup, reading your bill |
| Library | Embedding in Python |
| Named account profiles | Tracking a changing service agreement over time, importing PG&E statements |
| Bill calculator | Computing a cycle from interval meter data |
| MQTT | Publishing, with Home Assistant discovery |
| REST API | HTTP service |
| Home Assistant | Custom component, Energy dashboard, account history, response actions, opt-in Predbat |
| Home Assistant quality checklist | Self-assessment against the Integration Quality Scale, with exemptions |
| Containers | Local Home Assistant development stack and API/MQTT deployment proposal |
| Maintaining rate data | Regenerating export rates, updating the retail tariff and CCA cards |
| Packaging strategy | Architecture decision, boundaries, and release model |
| Release procedure | Versioning, Trusted Publishing, verification, and recovery |
Works with
Prices are published in the shapes these already read, via either the custom component or the MQTT publisher — no template plumbing on your side. The two surfaces differ for EMHASS and Predbat: MQTT always publishes their attributes, while the custom component asks for a window on demand and keeps Predbat opt-in.
| Custom component | MQTT | |
|---|---|---|
| Home Assistant Energy dashboard | Import/export price entities | Import/export price entities |
| EMHASS | tariffkit.get_emhass_forecast action, called with any window |
load_cost_forecast / prod_price_forecast attributes, always published |
| Predbat | raw_today / raw_tomorrow attributes, only once enabled in options |
raw_today / raw_tomorrow attributes, always published |
See docs/home-assistant.md and docs/mqtt.md for setup of each.
Install
pip install tariffkit # core, zero dependencies
pip install 'tariffkit[mqtt]' # + MQTT publisher with Home Assistant discovery
pip install 'tariffkit[web]' # + FastAPI service
pip install 'tariffkit[secrets]' # + OS keyring credential storage
pip install 'tariffkit[statements]' # + reading local PG&E statement PDFs
pip install 'tariffkit[all]'
The Home Assistant custom component is also available through HACS as a custom repository while its default-store submission is reviewed:
CLI
tariffkit now # current import/export price
tariffkit forecast --hours 48 # the upcoming curve
tariffkit forecast --format json # machine-readable
tariffkit mqtt --broker 192.168.1.100 # publish hourly, with HA discovery
tariffkit serve # REST API on :8000
tariffkit bill intervals.csv # compute a cycle from meter data
tariffkit info # which data is loaded, and from where
tariffkit account init home # track a service agreement's history
tariffkit account source home show ha # inspect profile grid-import/export entities
Configuration
Defaults target a PG&E-bundled residential customer. Point it at your own
service agreement via ~/.config/tariffkit/config.toml:
supplier = "bundled" # or "cca"
interconnection_year = 2026 # selects the NBT vintage and ACC Plus row
pto_date = "2026-06-03" # starts the nine-year rate lock
acc_plus_segment = "residential"
base_services_charge_tier = 3
TARIFFKIT_* environment variables override any file setting.
Long-lived PG&E, Home Assistant, InfluxDB, and MQTT credentials can be stored
outside that file with tariffkit credentials set; see
Configuration.
What the numbers include
- Import price is the marginal per-kWh cost: generation + distribution for
the season and period, plus the flat riders. The AB 205 Base Services Charge
is a fixed $/day amount and is deliberately excluded: folding it into a
$/kWh figure would corrupt any marginal dispatch decision. Read it separately
via
engine.daily_fixed_charge(). - Export credit is the generation component plus the delivery component,
plus your ACC Plus adder. Values past your nine-year lock are still returned
but flagged
locked=False; PG&E publishes them for illustration only.
Edge cases in PG&E's published data
Found by round-tripping the vendored matrices against all 1.75 million source rows. The library handles each; they are documented because they are surprising.
- The autumn DST hour. The fall-back day has 25 real hours but only 24 rate
labels. PG&E gives the repeated 01:00 PST the
HS2label, so it is priced as 2am. Pricing it by wall-clock hour returns the wrong value for that hour. - Far-future drift. From 2036 onward PG&E's own hour labels stop tracking
Pacific daylight time, and NBT25/26/00 duplicate some holidays onto the
following day. Each vintage records the last year verified exact; prices past
it are returned with
exact=False. Every year inside a nine-year rate lock is exact, and those years are already published as illustrative only. - Vintages disagree about holidays in those same late years, so the holiday calendar used for a lookup is the one embedded in that vintage's own file.
- Only the June 2026 tariff sheets are vendored, for each of the three schedules. Earlier timestamps raise rather than silently back-dating current rates onto an older billing period.
- E-TOU-C's baseline credit is not in the marginal price. It applies to the
first N kWh of a cycle, which is a quantity rather than a time, so
price_atreports it asbaseline_creditand the billing engine applies it.
CCA customers
If a Community Choice Aggregator supplies your generation, PG&E still delivers, and under NEM 3.0 you receive only the delivery component of the export credit from PG&E; generation compensation comes from the CCA.
An MCE rate card is vendored (generation by season/period, the Cost Relief Credit, Deep Green premium, and the 10% Solar Bonus Credit):
supplier = "cca"
[cca]
name = "MCE"
rate_card = "mce"
pcia_rate = 0.03476 # $/kWh, from your bill
franchise_fee_surcharge = 0.00042 # $/kWh, from your bill
For other CCAs, supply generation_rates directly; see
docs/configuration.md. Until generation rates and a
franchise fee are configured, CCA mode returns delivery-only prices flagged
complete = False rather than quietly understating your rates.
Note that a CCA customer's PCIA is a charge, while a bundled customer's is a credit, so CCA service can cost several cents per kWh more on import.
Keeping rates current
Every vendored dataset is regenerated from the document that publishes it, by
tools.regen. Nothing is hand-transcribed and nothing is hand-edited.
python -m tools.regen # rebuild every dataset
python -m tools.regen --check # exit 1 if a publisher moved
python -m tools.regen tariff --for-date 2025-12-15 # rebuild a superseded vintage
python -m tools.regen.export --download # the 843 MB export-rate archive
Export matrices come from PG&E's CSV archive, which is large enough to have its own entry point; everything else comes from a published PDF. Nothing is written unless the rendered file survives being read back by the library code that will consume it, so a generator that drifts from the schema fails instead of shipping.
A weekly CI job runs --check, so a rate change surfaces as a failing build
rather than as silent drift. Export files are updated by October 1 of any
year the CPUC adopts a new Avoided Cost Calculator; retail rates change more
often, via advice letters — three times in the first half of 2026 alone — which
is why a superseded vintage can be rebuilt from the filing that adopted it. See
docs/data.md.
Data sources
See docs/data.md for the source of every vendored table and why OpenEI's URDB is deliberately not used.
License
MIT
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 tariffkit-0.2.1.tar.gz.
File metadata
- Download URL: tariffkit-0.2.1.tar.gz
- Upload date:
- Size: 517.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f95d7f541ecaf70c2ee8fad28297d06c3879e8d1e74ac8e2746030ffdb1e23f6
|
|
| MD5 |
58b6931fd90e165aef9b08b4abccca80
|
|
| BLAKE2b-256 |
cf2da4827f9b1ed3ae015edc1078999b98d687ecf300c83ff528d4be02bfb092
|
Provenance
The following attestation bundles were made for tariffkit-0.2.1.tar.gz:
Publisher:
release.yml on eman/tariffkit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tariffkit-0.2.1.tar.gz -
Subject digest:
f95d7f541ecaf70c2ee8fad28297d06c3879e8d1e74ac8e2746030ffdb1e23f6 - Sigstore transparency entry: 2484616102
- Sigstore integration time:
-
Permalink:
eman/tariffkit@6a225116606d29f0c1f1c14b524dfaf6ec893721 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/eman
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@6a225116606d29f0c1f1c14b524dfaf6ec893721 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file tariffkit-0.2.1-py3-none-any.whl.
File metadata
- Download URL: tariffkit-0.2.1-py3-none-any.whl
- Upload date:
- Size: 479.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
667df66bda11d787a636764adbe56fc434083d645fbe828ea68c5152228df9ba
|
|
| MD5 |
fbeaf6bf544f4a624aada7de35951606
|
|
| BLAKE2b-256 |
59f36db5c76be767b1659fe022a5e9368c5dcdeece74a7ce344e7541374b9c6a
|
Provenance
The following attestation bundles were made for tariffkit-0.2.1-py3-none-any.whl:
Publisher:
release.yml on eman/tariffkit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tariffkit-0.2.1-py3-none-any.whl -
Subject digest:
667df66bda11d787a636764adbe56fc434083d645fbe828ea68c5152228df9ba - Sigstore transparency entry: 2484616294
- Sigstore integration time:
-
Permalink:
eman/tariffkit@6a225116606d29f0c1f1c14b524dfaf6ec893721 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/eman
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@6a225116606d29f0c1f1c14b524dfaf6ec893721 -
Trigger Event:
workflow_dispatch
-
Statement type: