Metadata-Version: 2.5
Name: tariffkit
Version: 0.2.0
Summary: Offline electricity tariff pricing, billing, and integration tools
Project-URL: Homepage, https://github.com/eman/tariffkit
Project-URL: Documentation, https://github.com/eman/tariffkit#documentation
Project-URL: Issues, https://github.com/eman/tariffkit/issues
Project-URL: Source, https://github.com/eman/tariffkit
License-Expression: MIT
License-File: LICENSE
Keywords: battery,energy,home-assistant,nem3,net-billing-tariff,pge,solar,tariff
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Home Automation
Classifier: Typing :: Typed
Requires-Python: >=3.14
Provides-Extra: all
Requires-Dist: fastapi>=0.110; extra == 'all'
Requires-Dist: httpx>=0.27; extra == 'all'
Requires-Dist: keyring>=25; extra == 'all'
Requires-Dist: paho-mqtt>=2.0; extra == 'all'
Requires-Dist: pypdf>=4.0; extra == 'all'
Requires-Dist: uvicorn[standard]>=0.29; extra == 'all'
Requires-Dist: websockets>=12; extra == 'all'
Provides-Extra: ha
Requires-Dist: websockets>=12; extra == 'ha'
Provides-Extra: influx
Requires-Dist: httpx>=0.27; extra == 'influx'
Provides-Extra: mqtt
Requires-Dist: paho-mqtt>=2.0; extra == 'mqtt'
Provides-Extra: pge
Requires-Dist: httpx>=0.27; extra == 'pge'
Provides-Extra: secrets
Requires-Dist: keyring>=25; extra == 'secrets'
Provides-Extra: statements
Requires-Dist: pypdf>=4.0; extra == 'statements'
Provides-Extra: web
Requires-Dist: fastapi>=0.110; extra == 'web'
Requires-Dist: uvicorn[standard]>=0.29; extra == 'web'
Description-Content-Type: text/markdown

# 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.

```python
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](docs/configuration.md) | Settings, CCA setup, reading your bill |
| [Library](docs/library.md) | Embedding in Python |
| [Named account profiles](docs/accounts.md) | Tracking a changing service agreement over time, importing PG&E statements |
| [Bill calculator](docs/billing.md) | Computing a cycle from interval meter data |
| [MQTT](docs/mqtt.md) | Publishing, with Home Assistant discovery |
| [REST API](docs/web.md) | HTTP service |
| [Home Assistant](docs/home-assistant.md) | Custom component, Energy dashboard, account history, response actions, opt-in Predbat |
| [Home Assistant quality checklist](docs/home-assistant-quality.md) | Self-assessment against the Integration Quality Scale, with exemptions |
| [Containers](docs/containers.md) | Local Home Assistant development stack and API/MQTT deployment proposal |
| [Maintaining rate data](docs/data.md) | Regenerating export rates, updating the retail tariff and CCA cards |
| [Packaging strategy](docs/packaging_strategy.md) | Architecture decision, boundaries, and release model |
| [Release procedure](docs/releases.md) | 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](docs/home-assistant.md) and
[docs/mqtt.md](docs/mqtt.md) for setup of each.

## Install

```bash
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]'
```

## CLI

```bash
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`:

```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](docs/configuration.md#credentials).

### 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 `HS2` label, 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_at`
  reports it as `baseline_credit` and 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):

```toml
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](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.

```bash
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](docs/data.md).

## Data sources

See [docs/data.md](docs/data.md#data-sources) for the source of every vendored
table and why OpenEI's URDB is deliberately not used.

## License

MIT
