tariffkit 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. tariffkit-0.2.0/.gitignore +33 -0
  2. tariffkit-0.2.0/CHANGELOG.md +172 -0
  3. tariffkit-0.2.0/LICENSE +21 -0
  4. tariffkit-0.2.0/PKG-INFO +260 -0
  5. tariffkit-0.2.0/README.md +215 -0
  6. tariffkit-0.2.0/pyproject.toml +152 -0
  7. tariffkit-0.2.0/src/tariffkit/__init__.py +52 -0
  8. tariffkit-0.2.0/src/tariffkit/account/__init__.py +41 -0
  9. tariffkit-0.2.0/src/tariffkit/account/cli.py +493 -0
  10. tariffkit-0.2.0/src/tariffkit/account/errors.py +25 -0
  11. tariffkit-0.2.0/src/tariffkit/account/model.py +641 -0
  12. tariffkit-0.2.0/src/tariffkit/account/rates.py +61 -0
  13. tariffkit-0.2.0/src/tariffkit/account/repository.py +329 -0
  14. tariffkit-0.2.0/src/tariffkit/billing/__init__.py +56 -0
  15. tariffkit-0.2.0/src/tariffkit/billing/engine.py +505 -0
  16. tariffkit-0.2.0/src/tariffkit/billing/ledger.py +365 -0
  17. tariffkit-0.2.0/src/tariffkit/billing/models.py +274 -0
  18. tariffkit-0.2.0/src/tariffkit/billing/netting.py +137 -0
  19. tariffkit-0.2.0/src/tariffkit/billing/trueup.py +498 -0
  20. tariffkit-0.2.0/src/tariffkit/cca.py +122 -0
  21. tariffkit-0.2.0/src/tariffkit/cli.py +809 -0
  22. tariffkit-0.2.0/src/tariffkit/config.py +296 -0
  23. tariffkit-0.2.0/src/tariffkit/data/__init__.py +40 -0
  24. tariffkit-0.2.0/src/tariffkit/data/cca/mce/2023-01-01.toml +61 -0
  25. tariffkit-0.2.0/src/tariffkit/data/cca/mce/2026-04-01.toml +152 -0
  26. tariffkit-0.2.0/src/tariffkit/data/export/pge/acc_plus/2023-04-15.toml +43 -0
  27. tariffkit-0.2.0/src/tariffkit/data/export/pge/nbt00.json.gz +0 -0
  28. tariffkit-0.2.0/src/tariffkit/data/export/pge/nbt23.json.gz +0 -0
  29. tariffkit-0.2.0/src/tariffkit/data/export/pge/nbt24.json.gz +0 -0
  30. tariffkit-0.2.0/src/tariffkit/data/export/pge/nbt25.json.gz +0 -0
  31. tariffkit-0.2.0/src/tariffkit/data/export/pge/nbt26.json.gz +0 -0
  32. tariffkit-0.2.0/src/tariffkit/data/holidays.toml +36 -0
  33. tariffkit-0.2.0/src/tariffkit/data/manifest.json +56 -0
  34. tariffkit-0.2.0/src/tariffkit/data/nsc/pge.toml +57 -0
  35. tariffkit-0.2.0/src/tariffkit/data/tariff/pge/eelec/2025-01-01.toml +151 -0
  36. tariffkit-0.2.0/src/tariffkit/data/tariff/pge/eelec/2025-03-01.toml +150 -0
  37. tariffkit-0.2.0/src/tariffkit/data/tariff/pge/eelec/2025-09-01.toml +150 -0
  38. tariffkit-0.2.0/src/tariffkit/data/tariff/pge/eelec/2026-01-01.toml +153 -0
  39. tariffkit-0.2.0/src/tariffkit/data/tariff/pge/eelec/2026-03-01.toml +157 -0
  40. tariffkit-0.2.0/src/tariffkit/data/tariff/pge/etouc/2025-01-01.toml +222 -0
  41. tariffkit-0.2.0/src/tariffkit/data/tariff/pge/etouc/2025-03-01.toml +221 -0
  42. tariffkit-0.2.0/src/tariffkit/data/tariff/pge/etouc/2025-09-01.toml +221 -0
  43. tariffkit-0.2.0/src/tariffkit/data/tariff/pge/etouc/2026-01-01.toml +224 -0
  44. tariffkit-0.2.0/src/tariffkit/data/tariff/pge/etouc/2026-03-01.toml +231 -0
  45. tariffkit-0.2.0/src/tariffkit/data/tariff/pge/ev2a/2025-01-01.toml +144 -0
  46. tariffkit-0.2.0/src/tariffkit/data/tariff/pge/ev2a/2025-03-01.toml +143 -0
  47. tariffkit-0.2.0/src/tariffkit/data/tariff/pge/ev2a/2025-09-01.toml +143 -0
  48. tariffkit-0.2.0/src/tariffkit/data/tariff/pge/ev2a/2026-01-01.toml +146 -0
  49. tariffkit-0.2.0/src/tariffkit/data/tariff/pge/ev2a/2026-03-01.toml +153 -0
  50. tariffkit-0.2.0/src/tariffkit/data/tax/ca_energy_resources/2025-01-01.toml +27 -0
  51. tariffkit-0.2.0/src/tariffkit/data/tax/ca_energy_resources/2026-01-01.toml +27 -0
  52. tariffkit-0.2.0/src/tariffkit/data/versioned.py +118 -0
  53. tariffkit-0.2.0/src/tariffkit/engine.py +82 -0
  54. tariffkit-0.2.0/src/tariffkit/errors.py +19 -0
  55. tariffkit-0.2.0/src/tariffkit/export/__init__.py +5 -0
  56. tariffkit-0.2.0/src/tariffkit/export/nbt.py +207 -0
  57. tariffkit-0.2.0/src/tariffkit/interop/__init__.py +21 -0
  58. tariffkit-0.2.0/src/tariffkit/interop/emhass.py +79 -0
  59. tariffkit-0.2.0/src/tariffkit/interop/predbat.py +102 -0
  60. tariffkit-0.2.0/src/tariffkit/interop/slots.py +63 -0
  61. tariffkit-0.2.0/src/tariffkit/models.py +164 -0
  62. tariffkit-0.2.0/src/tariffkit/mqtt/__init__.py +6 -0
  63. tariffkit-0.2.0/src/tariffkit/mqtt/discovery.py +84 -0
  64. tariffkit-0.2.0/src/tariffkit/mqtt/publisher.py +305 -0
  65. tariffkit-0.2.0/src/tariffkit/providers/__init__.py +1 -0
  66. tariffkit-0.2.0/src/tariffkit/providers/pge/__init__.py +33 -0
  67. tariffkit-0.2.0/src/tariffkit/providers/pge/reconcile.py +828 -0
  68. tariffkit-0.2.0/src/tariffkit/providers/pge/statements/__init__.py +26 -0
  69. tariffkit-0.2.0/src/tariffkit/providers/pge/statements/errors.py +20 -0
  70. tariffkit-0.2.0/src/tariffkit/providers/pge/statements/model.py +320 -0
  71. tariffkit-0.2.0/src/tariffkit/providers/pge/statements/ocr.py +193 -0
  72. tariffkit-0.2.0/src/tariffkit/providers/pge/statements/parse.py +813 -0
  73. tariffkit-0.2.0/src/tariffkit/py.typed +0 -0
  74. tariffkit-0.2.0/src/tariffkit/secrets.py +164 -0
  75. tariffkit-0.2.0/src/tariffkit/sources/__init__.py +71 -0
  76. tariffkit-0.2.0/src/tariffkit/sources/greenbutton.py +318 -0
  77. tariffkit-0.2.0/src/tariffkit/sources/homeassistant.py +342 -0
  78. tariffkit-0.2.0/src/tariffkit/sources/influx.py +359 -0
  79. tariffkit-0.2.0/src/tariffkit/sources/pge.py +1153 -0
  80. tariffkit-0.2.0/src/tariffkit/tariff/__init__.py +5 -0
  81. tariffkit-0.2.0/src/tariffkit/tariff/retail.py +271 -0
  82. tariffkit-0.2.0/src/tariffkit/timeutil.py +120 -0
  83. tariffkit-0.2.0/src/tariffkit/web/__init__.py +5 -0
  84. tariffkit-0.2.0/src/tariffkit/web/app.py +220 -0
  85. tariffkit-0.2.0/tests/conftest.py +26 -0
  86. tariffkit-0.2.0/tests/fixtures/nbt_golden_rows.jsonl +2120 -0
  87. tariffkit-0.2.0/tests/fixtures/statements/synthetic_cca_ratechange.txt +77 -0
  88. tariffkit-0.2.0/tests/test_account.py +354 -0
  89. tariffkit-0.2.0/tests/test_account_cli.py +422 -0
  90. tariffkit-0.2.0/tests/test_billing.py +533 -0
  91. tariffkit-0.2.0/tests/test_eelec.py +274 -0
  92. tariffkit-0.2.0/tests/test_engine.py +330 -0
  93. tariffkit-0.2.0/tests/test_ev2a.py +177 -0
  94. tariffkit-0.2.0/tests/test_export_golden.py +101 -0
  95. tariffkit-0.2.0/tests/test_integrations.py +209 -0
  96. tariffkit-0.2.0/tests/test_interop.py +322 -0
  97. tariffkit-0.2.0/tests/test_ledger.py +270 -0
  98. tariffkit-0.2.0/tests/test_mce.py +225 -0
  99. tariffkit-0.2.0/tests/test_mqtt_publisher.py +243 -0
  100. tariffkit-0.2.0/tests/test_pge_reconcile.py +387 -0
  101. tariffkit-0.2.0/tests/test_pge_statements.py +128 -0
  102. tariffkit-0.2.0/tests/test_private_config.py +193 -0
  103. tariffkit-0.2.0/tests/test_schedules.py +256 -0
  104. tariffkit-0.2.0/tests/test_sources_greenbutton.py +212 -0
  105. tariffkit-0.2.0/tests/test_sources_ha.py +372 -0
  106. tariffkit-0.2.0/tests/test_sources_influx.py +425 -0
  107. tariffkit-0.2.0/tests/test_sources_pge.py +300 -0
  108. tariffkit-0.2.0/tests/test_trueup.py +293 -0
  109. tariffkit-0.2.0/tests/test_versioned.py +137 -0
@@ -0,0 +1,33 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ venv/
9
+ .env
10
+ .env.bak*
11
+
12
+ # One real account's tariff history. audit/account.example.toml is the template.
13
+ audit/account.toml
14
+
15
+ # Statements carry a name, an address, an account number and a year of usage.
16
+ # They are read wherever they already sit and never committed; the audit tests
17
+ # use synthetic fixtures instead. Downloads land in .cache/, ignored above.
18
+ *.pdf
19
+
20
+ .pytest_cache/
21
+ .mypy_cache/
22
+ .ruff_cache/
23
+ .coverage
24
+ htmlcov/
25
+
26
+ # Upstream archives and documents fetched by `python -m tools.regen` — large, not
27
+ # vendored. Also where downloaded statements and Green Button exports land.
28
+ # The normalized outputs under src/tariffkit/data/ ARE committed.
29
+ /.cache/
30
+ *.zip
31
+ !tests/fixtures/*.zip
32
+
33
+ .DS_Store
@@ -0,0 +1,172 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. This project follows
4
+ [Semantic Versioning](https://semver.org/).
5
+
6
+ ## [Unreleased]
7
+
8
+ ## [0.2.0] - 2026-08-16
9
+
10
+ ### Added
11
+ - **Named, effective-dated account profiles** track tariff, supplier, baseline,
12
+ export, and credential-set changes over a service agreement's lifetime.
13
+ Pricing and billing resolve the settings in force at each timestamp, including
14
+ cycles that cross an account transition. Profile writes are atomic,
15
+ revision-checked, and locked against concurrent updates.
16
+ - **PG&E statement import and portal synchronization** can populate an account
17
+ profile from printed facts. Proposed changes are reported as additions,
18
+ confirmations, conflicts, or missing required values; a statement with a
19
+ conflict cannot be partially applied. Statement support is available through
20
+ the `statements` extra, while the account-specific audit harness remains
21
+ repository-only.
22
+ - **Profile-scoped meter sources** store provider-neutral grid-import and
23
+ grid-export mappings for Home Assistant and InfluxDB 3. The bill command can
24
+ query either source directly, with explicit command options taking precedence
25
+ over profile, environment, and global defaults.
26
+ - **Credential sets backed by the operating-system keyring** hold PG&E, Home
27
+ Assistant, InfluxDB, and MQTT secrets outside configuration files and command
28
+ arguments. Environment injection remains available for containers.
29
+ - **A complete billing layer** now covers interval netting, coverage warnings,
30
+ baseline credits, fixed charges, taxes, export-credit buckets, annual true-up,
31
+ and CCA cash-out. Credits retain their printed statement scope instead of
32
+ being treated as one fungible balance.
33
+ - **Home Assistant and InfluxDB 3 interval sources** complement the Green Button
34
+ reader. Home Assistant reads long-term statistics and prefers five-minute
35
+ data; InfluxDB derives exact totals from cumulative-counter endpoints and
36
+ spreads advances across the time in which they accrued.
37
+ - **E-TOU-C and EV2-A retail schedules**, complete PCIA and franchise-fee
38
+ vintage tables, E-TOU-C baseline allowances, California's electrical-energy
39
+ surcharge, Net Surplus Compensation data, and effective-dated PG&E and MCE
40
+ rate snapshots extend pricing and billing beyond the original E-ELEC
41
+ schedule.
42
+ - **Repository-only rate-data generators** rebuild every vendored dataset from
43
+ its published source and read the rendered result back through runtime code
44
+ before writing it. A weekly workflow checks export matrices, retail tariffs,
45
+ ACC Plus, CCA cards, Net Surplus Compensation, holidays, and the state
46
+ surcharge for upstream changes.
47
+ - **Home Assistant account-history flows** support staged initial setup and
48
+ profile inspection, transition editing, statement import, and sanitized
49
+ profile export. Stable profile-based config-entry identity survives tariff and
50
+ supplier changes.
51
+ - **Home Assistant response actions** provide current or forecast rates in
52
+ native and EMHASS shapes for caller-selected windows. Requests reject
53
+ ambiguous DST-fold timestamps, misaligned windows, and horizons beyond seven
54
+ days rather than guessing.
55
+ - **Home Assistant diagnostics and integration tests** cover config and options
56
+ flows, migration, entities, actions, Energy dashboard compatibility,
57
+ effective-dated provenance, DST handling, and opt-in Predbat output.
58
+ Diagnostics deliberately omit account history, observations, credentials,
59
+ and meter mappings.
60
+ - **Pure interoperability adapters** generate EMHASS, Predbat, and generic slot
61
+ payloads from a `PriceCurve`, shared by Home Assistant and MQTT without either
62
+ integration becoming a dependency.
63
+ - **Request-scoped REST configuration and account selection** allow callers to
64
+ price one request against validated settings or a named profile. Unknown
65
+ configuration keys and credential fields are rejected.
66
+ - **A bind-mounted Home Assistant development stack** runs the custom component
67
+ and local package source in a real container without publishing a wheel.
68
+ - **A build-once release process and maintainer runbook** synchronize Python,
69
+ lockfile, Home Assistant, changelog, and documentation versions. One validated
70
+ wheel/sdist pair moves through optional TestPyPI staging, protected PyPI
71
+ approval, PEP 740 attestations, and an immutable GitHub release.
72
+
73
+ ### Changed
74
+ - **The project is now TariffKit.** The distribution, import package, CLI,
75
+ configuration directory, environment prefix, MQTT namespace, repository
76
+ links, and Home Assistant domain use the utility-neutral `tariffkit` identity.
77
+ The initial data-provider scope remains PG&E and California.
78
+ - **Packaging remains one public distribution in one repository.** Optional
79
+ features use extras and lazy imports, leaving the default runtime
80
+ dependency-free. Rate-data tooling and the account-specific audit harness are
81
+ excluded from wheel and sdist but retain strict lint, typing, and test
82
+ coverage.
83
+ - **Python 3.14 is the minimum**, and Home Assistant 2026.3.0 is therefore the
84
+ integration floor. Development dependencies use PEP 735 groups, package
85
+ licensing uses an SPDX expression, and installed metadata is the runtime
86
+ version source.
87
+ - **Home Assistant installs the exact matching TariffKit release** instead of
88
+ vendoring package source. Its config flow asks only questions made relevant
89
+ by earlier answers and separates account history from forecast and Predbat
90
+ options.
91
+ - **Home Assistant entities are lean and recorder-safe.** Import and export
92
+ price sensors remain compatible with the Energy dashboard, a timestamp entity
93
+ reports forecast coverage, and large forecast or optimizer payloads are not
94
+ recorded every minute. The fixed daily charge is no longer represented as a
95
+ marginal price sensor.
96
+ - **EMHASS forecasts are requested through an action and Predbat output is
97
+ opt-in.** Neither large payload is computed and attached to every entity when
98
+ unused.
99
+ - **Pricing provenance and quality are preserved end to end.** Core, CLI, REST,
100
+ MQTT, and Home Assistant outputs retain `locked`, `exact`, and `complete`
101
+ states and describe contiguous provenance segments across effective-date
102
+ boundaries.
103
+ - **Green Button names now describe the format rather than its container.**
104
+ `read_green_button`, `GreenButtonLayout`, and `--source green-button` replace
105
+ generic CSV terminology; the legacy source spelling remains accepted.
106
+ - **Strict typing now covers the Home Assistant custom component and all
107
+ repository tools**, while Ruff enforces function annotations throughout the
108
+ repository.
109
+
110
+ ### Fixed
111
+ - Production publishing no longer inherits a false implicit failure from the
112
+ intentionally skipped optional TestPyPI job. The explicit release dependency
113
+ checks now permit protected PyPI and GitHub publication only after the build
114
+ and draft jobs themselves succeed.
115
+ - Account-profile updates no longer race between revision checks and filesystem
116
+ mutation, statement-derived histories reject gaps and overlaps, imported Home
117
+ Assistant profiles cannot replace an entry's stable identity, and MQTT
118
+ profile selection respects environment precedence.
119
+ - Baseline credits use each day and vintage's own rate, pre-PTO exports earn no
120
+ compensation, string supplier values are normalized before branching, and
121
+ historical pricing never borrows data from a future vintage.
122
+ - CCA generation rates are selected by PG&E schedule instead of silently using
123
+ E-ELEC values for every plan. PCIA, franchise-fee, MCE generation, cost-relief,
124
+ and premium data are resolved from their actual effective vintages.
125
+ - Billing coverage and interval stepping use absolute time across both DST
126
+ transitions. Repeated autumn timestamps are disambiguated during ingest, rate
127
+ points remain contiguous, and MQTT no longer sleeps through the second 01:00
128
+ hour.
129
+ - Green Button ingestion handles account preambles, split date/time columns,
130
+ unit-suffixed headers, explicit offsets, and repeated autumn wall times.
131
+ Home Assistant and InfluxDB sources report implausible resets and missing
132
+ coverage instead of inventing plausible energy.
133
+ - Export-rate lookups use each vintage's own holiday calendar and surface
134
+ publisher drift beyond the verified exact range rather than silently claiming
135
+ exact future values.
136
+ - Generator parsing no longer drops a PCIA row joined to the next page header,
137
+ and all generated files are validated through independent runtime readers
138
+ before replacing vendored data.
139
+
140
+ ## [0.1.0] - 2026-07-28
141
+
142
+ Initial release.
143
+
144
+ ### Added
145
+ - `RateEngine` with `price_now()`, `price_at()`, and `forecast()` for PG&E
146
+ E-ELEC import prices and NEM 3.0 / Net Billing Tariff export credits.
147
+ - Vendored rate data for all five NBT vintages (NBT23/24/25/26/00), collapsed
148
+ from PG&E's ~40 MB-per-vintage hourly files to 268 KiB total with verified
149
+ lossless round-tripping.
150
+ - ACC Plus adder as a first-class, separately reported component.
151
+ - CCA / Direct Access support: bundled generation and PCIA are dropped, and
152
+ delivery-only prices are flagged `complete=False` rather than understated.
153
+ - CLI: `now`, `forecast`, `info`, `mqtt`, `serve`.
154
+ - MQTT publisher with Home Assistant MQTT Discovery and a last-will
155
+ availability topic.
156
+ - FastAPI service under the `web` extra.
157
+ - HACS-installable Home Assistant custom component.
158
+ - `tools/regen_data.py` for refreshing vendored data, plus a weekly CI job that
159
+ fails when upstream rates change.
160
+
161
+ ### Notes on upstream data
162
+ - PG&E's export files label the repeated 01:00 on the autumn DST transition as
163
+ `HS2`, so that hour is priced as 2am. Handled in `timeutil.export_hour`.
164
+ - From 2036 onward PG&E's own hour labels stop tracking Pacific daylight time
165
+ and NBT25/26/00 duplicate some holidays onto the following day. The verified
166
+ boundary is recorded per vintage as `exact_through` and surfaced as
167
+ `ExportPrice.exact`. Every year within a nine-year rate lock is exact.
168
+ - Holiday calendars are extracted per vintage from the source data rather than
169
+ recomputed, because the vintage files disagree in far-future years.
170
+
171
+ [Unreleased]: https://github.com/eman/tariffkit/compare/v0.2.0...HEAD
172
+ [0.2.0]: https://github.com/eman/tariffkit/releases/tag/v0.2.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Emmanuel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,260 @@
1
+ Metadata-Version: 2.5
2
+ Name: tariffkit
3
+ Version: 0.2.0
4
+ Summary: Offline electricity tariff pricing, billing, and integration tools
5
+ Project-URL: Homepage, https://github.com/eman/tariffkit
6
+ Project-URL: Documentation, https://github.com/eman/tariffkit#documentation
7
+ Project-URL: Issues, https://github.com/eman/tariffkit/issues
8
+ Project-URL: Source, https://github.com/eman/tariffkit
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: battery,energy,home-assistant,nem3,net-billing-tariff,pge,solar,tariff
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Home Automation
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.14
21
+ Provides-Extra: all
22
+ Requires-Dist: fastapi>=0.110; extra == 'all'
23
+ Requires-Dist: httpx>=0.27; extra == 'all'
24
+ Requires-Dist: keyring>=25; extra == 'all'
25
+ Requires-Dist: paho-mqtt>=2.0; extra == 'all'
26
+ Requires-Dist: pypdf>=4.0; extra == 'all'
27
+ Requires-Dist: uvicorn[standard]>=0.29; extra == 'all'
28
+ Requires-Dist: websockets>=12; extra == 'all'
29
+ Provides-Extra: ha
30
+ Requires-Dist: websockets>=12; extra == 'ha'
31
+ Provides-Extra: influx
32
+ Requires-Dist: httpx>=0.27; extra == 'influx'
33
+ Provides-Extra: mqtt
34
+ Requires-Dist: paho-mqtt>=2.0; extra == 'mqtt'
35
+ Provides-Extra: pge
36
+ Requires-Dist: httpx>=0.27; extra == 'pge'
37
+ Provides-Extra: secrets
38
+ Requires-Dist: keyring>=25; extra == 'secrets'
39
+ Provides-Extra: statements
40
+ Requires-Dist: pypdf>=4.0; extra == 'statements'
41
+ Provides-Extra: web
42
+ Requires-Dist: fastapi>=0.110; extra == 'web'
43
+ Requires-Dist: uvicorn[standard]>=0.29; extra == 'web'
44
+ Description-Content-Type: text/markdown
45
+
46
+ # TariffKit
47
+
48
+ An offline electricity tariff engine for pricing, billing, and energy-system
49
+ integrations. The first data provider supports PG&E residential rate plans under
50
+ **NEM 3.0 / the Net Billing Tariff (NBT)**; the package identity is deliberately
51
+ not tied to one utility or tariff program.
52
+
53
+ Three schedules are vendored: **E-ELEC** (Electric Home), **E-TOU-C**
54
+ (Time-of-Use, peak 4–9 p.m. every day), and **EV2-A** (Home Charging).
55
+
56
+ Under NBT your export credit is not a time-of-use schedule: it is an hourly
57
+ Avoided Cost Calculator value that swings from about $0.06/kWh at midday to
58
+ about $1.19/kWh on an August evening. Knowing what a kWh is worth right now, and
59
+ what it will be worth over the next two days, is the input to every useful
60
+ solar-and-battery dispatch decision.
61
+
62
+ ```python
63
+ from tariffkit import RateEngine
64
+
65
+ engine = RateEngine()
66
+ point = engine.price_now()
67
+
68
+ print(point.import_price.total) # $/kWh to draw from the grid
69
+ print(point.export_price.total) # $/kWh earned by exporting
70
+ print(point.spread) # positive => exporting beats self-consuming
71
+
72
+ curve = engine.forecast(hours=48)
73
+ for hour in curve.best_export_hours(3):
74
+ print(hour.start, hour.export_price.total)
75
+ ```
76
+
77
+ ## Why it works offline
78
+
79
+ PG&E publishes 20 years of hourly export rates per vintage, as CPUC Resolution
80
+ E-5301 requires (roughly 40 MB of CSV per vintage). But that file is a lossless
81
+ expansion of a 576-cell matrix per year (12 months × 2 day types × 24 hours) per
82
+ component. `tariffkit` collapses it at build time, verifying losslessness cell by
83
+ cell, so the entire five-vintage dataset ships inside the wheel at **268 KiB**
84
+ and every lookup is a few list indexes.
85
+
86
+ The retail side is similar: these schedules' period boundaries are identical every day
87
+ of the week including holidays and do not shift by season, so an import price is
88
+ fully determined by `(season, hour)`.
89
+
90
+ Nothing here touches the network at runtime.
91
+
92
+ ## Documentation
93
+
94
+ | | |
95
+ |---|---|
96
+ | [Configuration](docs/configuration.md) | Settings, CCA setup, reading your bill |
97
+ | [Library](docs/library.md) | Embedding in Python |
98
+ | [Named account profiles](docs/accounts.md) | Tracking a changing service agreement over time, importing PG&E statements |
99
+ | [Bill calculator](docs/billing.md) | Computing a cycle from interval meter data |
100
+ | [MQTT](docs/mqtt.md) | Publishing, with Home Assistant discovery |
101
+ | [REST API](docs/web.md) | HTTP service |
102
+ | [Home Assistant](docs/home-assistant.md) | Custom component, Energy dashboard, account history, response actions, opt-in Predbat |
103
+ | [Home Assistant quality checklist](docs/home-assistant-quality.md) | Self-assessment against the Integration Quality Scale, with exemptions |
104
+ | [Containers](docs/containers.md) | Local Home Assistant development stack and API/MQTT deployment proposal |
105
+ | [Maintaining rate data](docs/data.md) | Regenerating export rates, updating the retail tariff and CCA cards |
106
+ | [Packaging strategy](docs/packaging_strategy.md) | Architecture decision, boundaries, and release model |
107
+ | [Release procedure](docs/releases.md) | Versioning, Trusted Publishing, verification, and recovery |
108
+
109
+ ## Works with
110
+
111
+ Prices are published in the shapes these already read, via either the custom
112
+ component or the MQTT publisher — no template plumbing on your side. The two
113
+ surfaces differ for EMHASS and Predbat: MQTT always publishes their
114
+ attributes, while the custom component asks for a window on demand and keeps
115
+ Predbat opt-in.
116
+
117
+ | | Custom component | MQTT |
118
+ |---|---|---|
119
+ | **Home Assistant Energy dashboard** | Import/export price entities | Import/export price entities |
120
+ | **EMHASS** | `tariffkit.get_emhass_forecast` action, called with any window | `load_cost_forecast` / `prod_price_forecast` attributes, always published |
121
+ | **Predbat** | `raw_today` / `raw_tomorrow` attributes, only once enabled in options | `raw_today` / `raw_tomorrow` attributes, always published |
122
+
123
+ See [docs/home-assistant.md](docs/home-assistant.md) and
124
+ [docs/mqtt.md](docs/mqtt.md) for setup of each.
125
+
126
+ ## Install
127
+
128
+ ```bash
129
+ pip install tariffkit # core, zero dependencies
130
+ pip install 'tariffkit[mqtt]' # + MQTT publisher with Home Assistant discovery
131
+ pip install 'tariffkit[web]' # + FastAPI service
132
+ pip install 'tariffkit[secrets]' # + OS keyring credential storage
133
+ pip install 'tariffkit[statements]' # + reading local PG&E statement PDFs
134
+ pip install 'tariffkit[all]'
135
+ ```
136
+
137
+ ## CLI
138
+
139
+ ```bash
140
+ tariffkit now # current import/export price
141
+ tariffkit forecast --hours 48 # the upcoming curve
142
+ tariffkit forecast --format json # machine-readable
143
+ tariffkit mqtt --broker 192.168.1.100 # publish hourly, with HA discovery
144
+ tariffkit serve # REST API on :8000
145
+ tariffkit bill intervals.csv # compute a cycle from meter data
146
+ tariffkit info # which data is loaded, and from where
147
+ tariffkit account init home # track a service agreement's history
148
+ tariffkit account source home show ha # inspect profile grid-import/export entities
149
+ ```
150
+
151
+ ## Configuration
152
+
153
+ Defaults target a PG&E-bundled residential customer. Point it at your own
154
+ service agreement via `~/.config/tariffkit/config.toml`:
155
+
156
+ ```toml
157
+ supplier = "bundled" # or "cca"
158
+ interconnection_year = 2026 # selects the NBT vintage and ACC Plus row
159
+ pto_date = "2026-06-03" # starts the nine-year rate lock
160
+ acc_plus_segment = "residential"
161
+ base_services_charge_tier = 3
162
+ ```
163
+
164
+ `TARIFFKIT_*` environment variables override any file setting.
165
+ Long-lived PG&E, Home Assistant, InfluxDB, and MQTT credentials can be stored
166
+ outside that file with `tariffkit credentials set`; see
167
+ [Configuration](docs/configuration.md#credentials).
168
+
169
+ ### What the numbers include
170
+
171
+ - **Import price** is the marginal per-kWh cost: generation + distribution for
172
+ the season and period, plus the flat riders. The AB 205 Base Services Charge
173
+ is a fixed $/day amount and is deliberately *excluded*: folding it into a
174
+ $/kWh figure would corrupt any marginal dispatch decision. Read it separately
175
+ via `engine.daily_fixed_charge()`.
176
+ - **Export credit** is the generation component plus the delivery component,
177
+ plus your ACC Plus adder. Values past your nine-year lock are still returned
178
+ but flagged `locked=False`; PG&E publishes them for illustration only.
179
+
180
+ ### Edge cases in PG&E's published data
181
+
182
+ Found by round-tripping the vendored matrices against all 1.75 million source
183
+ rows. The library handles each; they are documented because they are surprising.
184
+
185
+ - **The autumn DST hour.** The fall-back day has 25 real hours but only 24 rate
186
+ labels. PG&E gives the repeated 01:00 PST the `HS2` label, so it is priced as
187
+ 2am. Pricing it by wall-clock hour returns the wrong value for that hour.
188
+ - **Far-future drift.** From 2036 onward PG&E's own hour labels stop tracking
189
+ Pacific daylight time, and NBT25/26/00 duplicate some holidays onto the
190
+ following day. Each vintage records the last year verified exact; prices past
191
+ it are returned with `exact=False`. Every year inside a nine-year rate lock is
192
+ exact, and those years are already published as illustrative only.
193
+ - **Vintages disagree about holidays** in those same late years, so the holiday
194
+ calendar used for a lookup is the one embedded in that vintage's own file.
195
+ - **Only the June 2026 tariff sheets are vendored**, for each of the three
196
+ schedules. Earlier timestamps raise rather than silently back-dating current
197
+ rates onto an older billing period.
198
+ - **E-TOU-C's baseline credit is not in the marginal price.** It applies to the
199
+ first N kWh of a cycle, which is a quantity rather than a time, so `price_at`
200
+ reports it as `baseline_credit` and the billing engine applies it.
201
+
202
+ ### CCA customers
203
+
204
+ If a Community Choice Aggregator supplies your generation, PG&E still delivers,
205
+ and under NEM 3.0 you receive **only the delivery component** of the export
206
+ credit from PG&E; generation compensation comes from the CCA.
207
+
208
+ An MCE rate card is vendored (generation by season/period, the Cost Relief
209
+ Credit, Deep Green premium, and the 10% Solar Bonus Credit):
210
+
211
+ ```toml
212
+ supplier = "cca"
213
+
214
+ [cca]
215
+ name = "MCE"
216
+ rate_card = "mce"
217
+ pcia_rate = 0.03476 # $/kWh, from your bill
218
+ franchise_fee_surcharge = 0.00042 # $/kWh, from your bill
219
+ ```
220
+
221
+ For other CCAs, supply `generation_rates` directly; see
222
+ [docs/configuration.md](docs/configuration.md). Until generation rates and a
223
+ franchise fee are configured, CCA mode returns delivery-only prices flagged
224
+ `complete = False` rather than quietly understating your rates.
225
+
226
+ Note that a CCA customer's PCIA is a **charge**, while a bundled customer's is a
227
+ **credit**, so CCA service can cost several cents per kWh more on import.
228
+
229
+ ## Keeping rates current
230
+
231
+ Every vendored dataset is regenerated from the document that publishes it, by
232
+ `tools.regen`. Nothing is hand-transcribed and nothing is hand-edited.
233
+
234
+ ```bash
235
+ python -m tools.regen # rebuild every dataset
236
+ python -m tools.regen --check # exit 1 if a publisher moved
237
+ python -m tools.regen tariff --for-date 2025-12-15 # rebuild a superseded vintage
238
+ python -m tools.regen.export --download # the 843 MB export-rate archive
239
+ ```
240
+
241
+ Export matrices come from PG&E's CSV archive, which is large enough to have its
242
+ own entry point; everything else comes from a published PDF. Nothing is written
243
+ unless the rendered file survives being read back by the library code that will
244
+ consume it, so a generator that drifts from the schema fails instead of shipping.
245
+
246
+ A weekly CI job runs `--check`, so a rate change surfaces as a failing build
247
+ rather than as silent drift. Export files are updated by **October 1** of any
248
+ year the CPUC adopts a new Avoided Cost Calculator; retail rates change more
249
+ often, via advice letters — three times in the first half of 2026 alone — which
250
+ is why a superseded vintage can be rebuilt from the filing that adopted it. See
251
+ [docs/data.md](docs/data.md).
252
+
253
+ ## Data sources
254
+
255
+ See [docs/data.md](docs/data.md#data-sources) for the source of every vendored
256
+ table and why OpenEI's URDB is deliberately not used.
257
+
258
+ ## License
259
+
260
+ MIT