tariffkit 0.2.1__tar.gz → 0.2.3__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.
- {tariffkit-0.2.1 → tariffkit-0.2.3}/CHANGELOG.md +59 -1
- {tariffkit-0.2.1 → tariffkit-0.2.3}/PKG-INFO +25 -4
- {tariffkit-0.2.1 → tariffkit-0.2.3}/README.md +23 -2
- {tariffkit-0.2.1 → tariffkit-0.2.3}/pyproject.toml +6 -2
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/__init__.py +2 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/cca.py +7 -1
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/cli.py +7 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/config.py +4 -3
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/cca/mce/2023-01-01.toml +1 -1
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/cca/mce/2026-04-01.toml +1 -1
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/export/pge/acc_plus/2023-04-15.toml +2 -6
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/nsc/pge.toml +2 -2
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/eelec/2025-01-01.toml +1 -1
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/eelec/2025-03-01.toml +1 -1
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/eelec/2025-09-01.toml +1 -1
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/eelec/2026-01-01.toml +1 -1
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/eelec/2026-03-01.toml +2 -2
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/etouc/2025-01-01.toml +1 -1
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/etouc/2025-03-01.toml +1 -1
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/etouc/2025-09-01.toml +1 -1
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/etouc/2026-01-01.toml +1 -1
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/etouc/2026-03-01.toml +6 -4
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/ev2a/2025-01-01.toml +1 -1
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/ev2a/2025-03-01.toml +1 -1
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/ev2a/2025-09-01.toml +1 -1
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/ev2a/2026-01-01.toml +1 -1
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/ev2a/2026-03-01.toml +2 -2
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/engine.py +6 -2
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/export/nbt.py +5 -4
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/models.py +34 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/mqtt/discovery.py +5 -2
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/mqtt/publisher.py +29 -1
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/tariff/retail.py +11 -6
- {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_account_cli.py +12 -1
- {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_eelec.py +10 -6
- {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_engine.py +26 -2
- {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_integrations.py +4 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_mce.py +6 -2
- {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_mqtt_publisher.py +92 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_private_config.py +4 -1
- {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_schedules.py +12 -7
- {tariffkit-0.2.1 → tariffkit-0.2.3}/.gitignore +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/LICENSE +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/account/__init__.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/account/cli.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/account/errors.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/account/model.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/account/rates.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/account/repository.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/billing/__init__.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/billing/engine.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/billing/ledger.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/billing/models.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/billing/netting.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/billing/trueup.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/__init__.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/export/pge/nbt00.json.gz +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/export/pge/nbt23.json.gz +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/export/pge/nbt24.json.gz +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/export/pge/nbt25.json.gz +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/export/pge/nbt26.json.gz +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/holidays.toml +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/manifest.json +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tax/ca_energy_resources/2025-01-01.toml +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tax/ca_energy_resources/2026-01-01.toml +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/versioned.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/errors.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/export/__init__.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/interop/__init__.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/interop/emhass.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/interop/predbat.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/interop/slots.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/mqtt/__init__.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/providers/__init__.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/providers/pge/__init__.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/providers/pge/reconcile.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/providers/pge/statements/__init__.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/providers/pge/statements/errors.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/providers/pge/statements/model.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/providers/pge/statements/ocr.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/providers/pge/statements/parse.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/py.typed +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/secrets.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/sources/__init__.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/sources/greenbutton.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/sources/homeassistant.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/sources/influx.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/sources/pge.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/tariff/__init__.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/timeutil.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/web/__init__.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/web/app.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/conftest.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/fixtures/nbt_golden_rows.jsonl +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/fixtures/statements/synthetic_cca_ratechange.txt +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_account.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_billing.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_ev2a.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_export_golden.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_interop.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_ledger.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_pge_reconcile.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_pge_statements.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_sources_greenbutton.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_sources_ha.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_sources_influx.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_sources_pge.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_trueup.py +0 -0
- {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_versioned.py +0 -0
|
@@ -5,6 +5,62 @@ All notable changes to this project are documented here. This project follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.2.3] - 2026-08-17
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Pacific Gas and Electric now has the unambiguous machine identifier
|
|
12
|
+
`pacific_gas_and_electric`, while Home Assistant and MQTT present `PG&E` or
|
|
13
|
+
the full company name. The separate `pge` identifier correctly means Portland
|
|
14
|
+
General Electric and is recognized but explicitly unsupported for pricing,
|
|
15
|
+
preventing it from ever selecting California tariff data.
|
|
16
|
+
- Home Assistant now labels the export-minus-import calculation explicitly,
|
|
17
|
+
translates time-of-use states for display, and presents the forecast horizon
|
|
18
|
+
as **Rates Available Through**. Forecast metadata and a new **Rate Data
|
|
19
|
+
Status** entity are grouped under diagnostics, where PTO date, export lock
|
|
20
|
+
end, NBT vintage, tariff provenance, source, and quality flags explain the
|
|
21
|
+
active rates without crowding the primary price controls.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- The PyPI project page now loads the TariffKit banner from an absolute URL
|
|
25
|
+
instead of an unresolved repository-relative path.
|
|
26
|
+
|
|
27
|
+
## [0.2.2] - 2026-08-16
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- Project documentation now uses a TariffKit banner built from the integration's
|
|
31
|
+
existing icon and a provider-neutral electricity rate curve.
|
|
32
|
+
- Public contribution and support guidance now provides privacy-safe issue
|
|
33
|
+
forms, private security routing, review ownership, a pull request checklist,
|
|
34
|
+
community conduct expectations, and exact development checks. The guidance
|
|
35
|
+
also makes the generated-data and repository-only audit boundaries explicit
|
|
36
|
+
so public collaboration does not expose utility-account material or alter
|
|
37
|
+
distribution guarantees.
|
|
38
|
+
- Pull requests now run GitHub's dependency review action with read-only
|
|
39
|
+
permissions so vulnerable or disallowed dependency changes fail before merge.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
- **Python 3.14.2 and Home Assistant 2026.3.0 are now the supported floors.**
|
|
43
|
+
The lockfile no longer carries the obsolete Home Assistant 2026.2 fallback,
|
|
44
|
+
and CI audits the complete locked dependency graph with a pinned `pip-audit`
|
|
45
|
+
release while retaining raw reports as failure artifacts. The Linux secrets
|
|
46
|
+
extra and Home Assistant tests both inherit the same exact cryptography pin;
|
|
47
|
+
an expiring policy requires each audit to report exactly its three known,
|
|
48
|
+
unreachable advisories and rejects any additional finding.
|
|
49
|
+
- The README now explains that default HACS approval may take months and gives
|
|
50
|
+
complete custom-repository installation steps for use during the review.
|
|
51
|
+
- Workflow actions are pinned to reviewed immutable commits, checkout credentials
|
|
52
|
+
are not persisted, superseded runs are bounded by concurrency controls, and
|
|
53
|
+
repository write access is isolated to the jobs that publish results.
|
|
54
|
+
- MQTT now rejects credentials over plaintext unless the operator explicitly
|
|
55
|
+
allows insecure authentication for an isolated trusted network. Passwords
|
|
56
|
+
also require a username, while anonymous plaintext publishing remains valid.
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
- Rate-sheet regeneration now scans trailing table cells with a linear parser,
|
|
60
|
+
avoiding pathological regular-expression backtracking on malformed publisher
|
|
61
|
+
text while preserving accepted dollar, decimal, negative, and change-marker
|
|
62
|
+
forms.
|
|
63
|
+
|
|
8
64
|
## [0.2.1] - 2026-08-16
|
|
9
65
|
|
|
10
66
|
### Added
|
|
@@ -184,6 +240,8 @@ Initial release.
|
|
|
184
240
|
- Holiday calendars are extracted per vintage from the source data rather than
|
|
185
241
|
recomputed, because the vintage files disagree in far-future years.
|
|
186
242
|
|
|
187
|
-
[Unreleased]: https://github.com/eman/tariffkit/compare/v0.2.
|
|
243
|
+
[Unreleased]: https://github.com/eman/tariffkit/compare/v0.2.3...HEAD
|
|
244
|
+
[0.2.3]: https://github.com/eman/tariffkit/releases/tag/v0.2.3
|
|
245
|
+
[0.2.2]: https://github.com/eman/tariffkit/releases/tag/v0.2.2
|
|
188
246
|
[0.2.1]: https://github.com/eman/tariffkit/releases/tag/v0.2.1
|
|
189
247
|
[0.2.0]: https://github.com/eman/tariffkit/releases/tag/v0.2.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tariffkit
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Offline electricity tariff pricing, billing, and integration tools
|
|
5
5
|
Project-URL: Homepage, https://github.com/eman/tariffkit
|
|
6
6
|
Project-URL: Documentation, https://github.com/eman/tariffkit#documentation
|
|
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.14
|
|
18
18
|
Classifier: Topic :: Home Automation
|
|
19
19
|
Classifier: Typing :: Typed
|
|
20
|
-
Requires-Python: >=3.14
|
|
20
|
+
Requires-Python: >=3.14.2
|
|
21
21
|
Provides-Extra: all
|
|
22
22
|
Requires-Dist: fastapi>=0.110; extra == 'all'
|
|
23
23
|
Requires-Dist: httpx>=0.27; extra == 'all'
|
|
@@ -43,8 +43,17 @@ Requires-Dist: fastapi>=0.110; extra == 'web'
|
|
|
43
43
|
Requires-Dist: uvicorn[standard]>=0.29; extra == 'web'
|
|
44
44
|
Description-Content-Type: text/markdown
|
|
45
45
|
|
|
46
|
+
<p align="center">
|
|
47
|
+
<img src="https://raw.githubusercontent.com/eman/tariffkit/main/images/banner.svg" alt="TariffKit — electricity pricing, billing, and energy integrations" width="100%">
|
|
48
|
+
</p>
|
|
49
|
+
|
|
46
50
|
# TariffKit
|
|
47
51
|
|
|
52
|
+
[](https://github.com/eman/tariffkit/actions/workflows/ci.yml)
|
|
53
|
+
[](https://pypi.org/project/tariffkit/)
|
|
54
|
+
[](https://www.python.org/downloads/)
|
|
55
|
+
[](LICENSE)
|
|
56
|
+
|
|
48
57
|
An offline electricity tariff engine for pricing, billing, and energy-system
|
|
49
58
|
integrations. The first data provider supports PG&E residential rate plans under
|
|
50
59
|
**NEM 3.0 / the Net Billing Tariff (NBT)**; the package identity is deliberately
|
|
@@ -134,11 +143,23 @@ pip install 'tariffkit[statements]' # + reading local PG&E statement PDFs
|
|
|
134
143
|
pip install 'tariffkit[all]'
|
|
135
144
|
```
|
|
136
145
|
|
|
137
|
-
The Home Assistant
|
|
138
|
-
|
|
146
|
+
The Home Assistant integration has been
|
|
147
|
+
[submitted to the default HACS store](https://github.com/hacs/default/pull/10019),
|
|
148
|
+
but approval queues can take months. Until it is approved, install TariffKit as
|
|
149
|
+
a custom repository:
|
|
139
150
|
|
|
140
151
|
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=eman&repository=tariffkit&category=integration)
|
|
141
152
|
|
|
153
|
+
Or add it manually:
|
|
154
|
+
|
|
155
|
+
1. Open **HACS** in Home Assistant.
|
|
156
|
+
2. Select the **three-dot menu → Custom repositories**.
|
|
157
|
+
3. Enter `https://github.com/eman/tariffkit`, choose **Integration**, and select
|
|
158
|
+
**Add**.
|
|
159
|
+
4. Search HACS for **TariffKit**, open it, and select **Download**.
|
|
160
|
+
5. Restart Home Assistant, then add TariffKit from
|
|
161
|
+
**Settings → Devices & services → Add integration**.
|
|
162
|
+
|
|
142
163
|
## CLI
|
|
143
164
|
|
|
144
165
|
```bash
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/eman/tariffkit/main/images/banner.svg" alt="TariffKit — electricity pricing, billing, and energy integrations" width="100%">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# TariffKit
|
|
2
6
|
|
|
7
|
+
[](https://github.com/eman/tariffkit/actions/workflows/ci.yml)
|
|
8
|
+
[](https://pypi.org/project/tariffkit/)
|
|
9
|
+
[](https://www.python.org/downloads/)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
|
|
3
12
|
An offline electricity tariff engine for pricing, billing, and energy-system
|
|
4
13
|
integrations. The first data provider supports PG&E residential rate plans under
|
|
5
14
|
**NEM 3.0 / the Net Billing Tariff (NBT)**; the package identity is deliberately
|
|
@@ -89,11 +98,23 @@ pip install 'tariffkit[statements]' # + reading local PG&E statement PDFs
|
|
|
89
98
|
pip install 'tariffkit[all]'
|
|
90
99
|
```
|
|
91
100
|
|
|
92
|
-
The Home Assistant
|
|
93
|
-
|
|
101
|
+
The Home Assistant integration has been
|
|
102
|
+
[submitted to the default HACS store](https://github.com/hacs/default/pull/10019),
|
|
103
|
+
but approval queues can take months. Until it is approved, install TariffKit as
|
|
104
|
+
a custom repository:
|
|
94
105
|
|
|
95
106
|
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=eman&repository=tariffkit&category=integration)
|
|
96
107
|
|
|
108
|
+
Or add it manually:
|
|
109
|
+
|
|
110
|
+
1. Open **HACS** in Home Assistant.
|
|
111
|
+
2. Select the **three-dot menu → Custom repositories**.
|
|
112
|
+
3. Enter `https://github.com/eman/tariffkit`, choose **Integration**, and select
|
|
113
|
+
**Add**.
|
|
114
|
+
4. Search HACS for **TariffKit**, open it, and select **Download**.
|
|
115
|
+
5. Restart Home Assistant, then add TariffKit from
|
|
116
|
+
**Settings → Devices & services → Add integration**.
|
|
117
|
+
|
|
97
118
|
## CLI
|
|
98
119
|
|
|
99
120
|
```bash
|
|
@@ -4,12 +4,12 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tariffkit"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.3"
|
|
8
8
|
description = "Offline electricity tariff pricing, billing, and integration tools"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
license-files = ["LICENSE"]
|
|
12
|
-
requires-python = ">=3.14"
|
|
12
|
+
requires-python = ">=3.14.2"
|
|
13
13
|
keywords = ["pge", "nem3", "net-billing-tariff", "solar", "battery", "home-assistant", "energy", "tariff"]
|
|
14
14
|
classifiers = [
|
|
15
15
|
"Development Status :: 3 - Alpha",
|
|
@@ -61,11 +61,15 @@ release = [
|
|
|
61
61
|
"packaging>=25",
|
|
62
62
|
"twine>=6.2",
|
|
63
63
|
]
|
|
64
|
+
security = [
|
|
65
|
+
"pip-audit==2.10.1",
|
|
66
|
+
]
|
|
64
67
|
dev = [
|
|
65
68
|
{ include-group = "test" },
|
|
66
69
|
{ include-group = "quality" },
|
|
67
70
|
{ include-group = "regen" },
|
|
68
71
|
{ include-group = "release" },
|
|
72
|
+
{ include-group = "security" },
|
|
69
73
|
"fastapi>=0.110",
|
|
70
74
|
"paho-mqtt>=2.0",
|
|
71
75
|
"pytest-homeassistant-custom-component>=0.13.316",
|
|
@@ -25,6 +25,7 @@ from .models import (
|
|
|
25
25
|
Season,
|
|
26
26
|
Supplier,
|
|
27
27
|
TouPeriod,
|
|
28
|
+
Utility,
|
|
28
29
|
)
|
|
29
30
|
from .timeutil import PACIFIC, DayType, now_pacific
|
|
30
31
|
|
|
@@ -47,6 +48,7 @@ __all__ = [
|
|
|
47
48
|
"Supplier",
|
|
48
49
|
"TariffKitError",
|
|
49
50
|
"TouPeriod",
|
|
51
|
+
"Utility",
|
|
50
52
|
"__version__",
|
|
51
53
|
"now_pacific",
|
|
52
54
|
]
|
|
@@ -15,6 +15,7 @@ from typing import Any
|
|
|
15
15
|
|
|
16
16
|
from .data import versioned
|
|
17
17
|
from .errors import ConfigError, DataError
|
|
18
|
+
from .models import Utility
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
@dataclass(frozen=True, slots=True)
|
|
@@ -115,7 +116,12 @@ def load_rate_card(provider: str, on: date) -> CcaRateCard:
|
|
|
115
116
|
) from exc
|
|
116
117
|
if version.raw.get("schema") != 1:
|
|
117
118
|
raise DataError(f"{provider}: unsupported rate card schema {version.raw.get('schema')}")
|
|
118
|
-
|
|
119
|
+
raw = dict(version.raw)
|
|
120
|
+
try:
|
|
121
|
+
raw["utility"] = Utility(raw["utility"]).value
|
|
122
|
+
except (KeyError, ValueError) as exc:
|
|
123
|
+
raise DataError(f"{provider}: invalid utility identifier") from exc
|
|
124
|
+
return CcaRateCard(provider=str(raw["provider"]), raw=raw)
|
|
119
125
|
|
|
120
126
|
|
|
121
127
|
def available_rate_cards() -> tuple[str, ...]:
|
|
@@ -215,6 +215,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
215
215
|
mqtt.add_argument("--topic-prefix")
|
|
216
216
|
mqtt.add_argument("--forecast-hours", type=int)
|
|
217
217
|
mqtt.add_argument("--tls", action="store_true", default=None)
|
|
218
|
+
mqtt.add_argument(
|
|
219
|
+
"--allow-insecure-auth",
|
|
220
|
+
action="store_true",
|
|
221
|
+
default=None,
|
|
222
|
+
help="allow MQTT credentials without TLS on an isolated trusted network",
|
|
223
|
+
)
|
|
218
224
|
mqtt.add_argument(
|
|
219
225
|
"--no-discovery",
|
|
220
226
|
dest="discovery",
|
|
@@ -601,6 +607,7 @@ def _mqtt_settings(args: Any, *, config: Config | None, profile_name: str | None
|
|
|
601
607
|
discovery=args.discovery,
|
|
602
608
|
forecast_hours=args.forecast_hours,
|
|
603
609
|
tls=args.tls,
|
|
610
|
+
allow_insecure_auth=args.allow_insecure_auth,
|
|
604
611
|
profile=profile_name,
|
|
605
612
|
)
|
|
606
613
|
if config is not None and settings.profile is not None:
|
|
@@ -11,7 +11,7 @@ from pathlib import Path
|
|
|
11
11
|
from typing import Any, Literal
|
|
12
12
|
|
|
13
13
|
from .errors import ConfigError
|
|
14
|
-
from .models import Supplier
|
|
14
|
+
from .models import Supplier, Utility
|
|
15
15
|
|
|
16
16
|
AccPlusSegment = Literal["residential", "residential_low_income", "none"]
|
|
17
17
|
Discount = Literal["none", "care", "fera"]
|
|
@@ -80,7 +80,7 @@ class CcaConfig:
|
|
|
80
80
|
class Config:
|
|
81
81
|
"""Everything needed to price a kWh for one service agreement."""
|
|
82
82
|
|
|
83
|
-
utility:
|
|
83
|
+
utility: Utility = Utility.PACIFIC_GAS_AND_ELECTRIC
|
|
84
84
|
tariff: str = "E-ELEC"
|
|
85
85
|
supplier: Supplier = Supplier.BUNDLED
|
|
86
86
|
|
|
@@ -117,6 +117,7 @@ class Config:
|
|
|
117
117
|
nsc_rate: float | None = None
|
|
118
118
|
|
|
119
119
|
def __post_init__(self) -> None:
|
|
120
|
+
object.__setattr__(self, "utility", Utility(self.utility))
|
|
120
121
|
# Coerce the string forms of the enums. Supplier is a StrEnum, so a
|
|
121
122
|
# plain "cca" compares equal to Supplier.CCA but is not it -- and every
|
|
122
123
|
# branch that matters tests identity. Constructing Config directly with
|
|
@@ -195,7 +196,7 @@ class Config:
|
|
|
195
196
|
def to_dict(self) -> dict[str, Any]:
|
|
196
197
|
"""A JSON-compatible representation suitable for API request bodies."""
|
|
197
198
|
data: dict[str, Any] = {
|
|
198
|
-
"utility": self.utility,
|
|
199
|
+
"utility": self.utility.value,
|
|
199
200
|
"tariff": self.tariff,
|
|
200
201
|
"supplier": self.supplier.value,
|
|
201
202
|
"interconnection_year": self.interconnection_year,
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
schema = 1
|
|
50
50
|
provider = "MCE"
|
|
51
51
|
name = "Marin Clean Energy"
|
|
52
|
-
utility = "
|
|
52
|
+
utility = "pacific_gas_and_electric"
|
|
53
53
|
#: MCE prices each PG&E schedule separately, so a rate card is keyed by schedule
|
|
54
54
|
#: rather than tied to one. Slugs match the tariff data directories.
|
|
55
55
|
schedules = ["E-ELEC", "E-TOU-C", "EV2-A"]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# ACC Plus adder, Pacific Gas and Electric Schedule NBT.
|
|
1
|
+
# ACC Plus adder, Pacific Gas and Electric Company Schedule NBT.
|
|
2
2
|
#
|
|
3
3
|
# Source: https://www.pge.com/tariffs/assets/pdf/tariffbook/ELEC_SCHEDS_NBT.pdf
|
|
4
4
|
#
|
|
@@ -18,13 +18,9 @@
|
|
|
18
18
|
# closure.
|
|
19
19
|
|
|
20
20
|
schema = 1
|
|
21
|
-
# When this version of the adder table took force. The Net Billing Tariff opened
|
|
22
|
-
# on 2023-04-15 and the five-year step-down has been published unchanged since,
|
|
23
|
-
# so there is one vintage -- but it is dated, because a revision must be able to
|
|
24
|
-
# sit beside it rather than overwrite it.
|
|
25
21
|
effective = "2023-04-15"
|
|
26
22
|
unit = "USD/kWh"
|
|
27
|
-
source = "Pacific Gas and Electric Schedule NBT"
|
|
23
|
+
source = "Pacific Gas and Electric Company Schedule NBT"
|
|
28
24
|
|
|
29
25
|
[residential]
|
|
30
26
|
2023 = 0.02200
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Pacific Gas and Electric Net Surplus Compensation (NSC) rates, by true-up month.
|
|
1
|
+
# Pacific Gas and Electric Company Net Surplus Compensation (NSC) rates, by true-up month.
|
|
2
2
|
#
|
|
3
3
|
# Source: https://www.pge.com/assets/pge/docs/clean-energy/solar/AB920-RateTable.pdf
|
|
4
4
|
#
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
# case rather than an edge one.
|
|
29
29
|
|
|
30
30
|
schema = 1
|
|
31
|
-
utility = "
|
|
31
|
+
utility = "pacific_gas_and_electric"
|
|
32
32
|
currency = "USD/kWh"
|
|
33
33
|
source_url = "https://www.pge.com/assets/pge/docs/clean-energy/solar/AB920-RateTable.pdf"
|
|
34
34
|
decision = "D.11-06-016"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Pacific Gas and Electric Schedule E-ELEC -- retail time-of-use rates.
|
|
1
|
+
# Pacific Gas and Electric Company Schedule E-ELEC -- retail time-of-use rates.
|
|
2
2
|
#
|
|
3
3
|
# Source: https://www.pge.com/tariffs/assets/pdf/tariffbook/ELEC_SCHEDS_E-ELEC.pdf
|
|
4
4
|
#
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
# makes a silent mis-parse essentially impossible.
|
|
23
23
|
|
|
24
24
|
schema = 1
|
|
25
|
-
utility = "
|
|
25
|
+
utility = "pacific_gas_and_electric"
|
|
26
26
|
tariff = "E-ELEC"
|
|
27
27
|
effective = "2026-03-01"
|
|
28
28
|
advice_letter = "7846-E"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Pacific Gas and Electric Company Schedule E-TOU-C -- retail time-of-use rates.
|
|
2
2
|
#
|
|
3
3
|
# Source: https://www.pge.com/tariffs/assets/pdf/tariffbook/ELEC_SCHEDS_E-TOU-C.pdf
|
|
4
4
|
#
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
# makes a silent mis-parse essentially impossible.
|
|
27
27
|
|
|
28
28
|
schema = 1
|
|
29
|
-
utility = "
|
|
29
|
+
utility = "pacific_gas_and_electric"
|
|
30
30
|
tariff = "E-TOU-C"
|
|
31
31
|
effective = "2026-03-01"
|
|
32
32
|
advice_letter = "7846-E"
|
|
@@ -193,8 +193,10 @@ drop_components = ["generation", "bundled_pcia"]
|
|
|
193
193
|
2025 = -0.01011
|
|
194
194
|
2026 = -0.01011
|
|
195
195
|
|
|
196
|
-
#
|
|
197
|
-
#
|
|
196
|
+
# Franchise fee surcharge, which a CCA/DA customer pays and a bundled one
|
|
197
|
+
# does not. Published in a separate schedule (E-FFS) and read from it,
|
|
198
|
+
# because carrying it forward would let that schedule be reissued without
|
|
199
|
+
# anything noticing -- it is live rate data on every CCA price.
|
|
198
200
|
[cca.franchise_fee_vintages]
|
|
199
201
|
2009 = 0.00064
|
|
200
202
|
2010 = 0.00061
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Pacific Gas and Electric Schedule EV2-A -- retail time-of-use rates.
|
|
1
|
+
# Pacific Gas and Electric Company Schedule EV2-A -- retail time-of-use rates.
|
|
2
2
|
#
|
|
3
3
|
# Source: https://www.pge.com/tariffs/assets/pdf/tariffbook/ELEC_SCHEDS_EV2%20(Sch).pdf
|
|
4
4
|
#
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
# makes a silent mis-parse essentially impossible.
|
|
24
24
|
|
|
25
25
|
schema = 1
|
|
26
|
-
utility = "
|
|
26
|
+
utility = "pacific_gas_and_electric"
|
|
27
27
|
tariff = "EV2-A"
|
|
28
28
|
effective = "2026-03-01"
|
|
29
29
|
advice_letter = "7846-E"
|
|
@@ -5,8 +5,9 @@ from __future__ import annotations
|
|
|
5
5
|
from datetime import UTC, datetime, timedelta
|
|
6
6
|
|
|
7
7
|
from .config import Config
|
|
8
|
+
from .errors import DataError
|
|
8
9
|
from .export.nbt import NbtExportRates
|
|
9
|
-
from .models import PriceCurve, PricePoint
|
|
10
|
+
from .models import PriceCurve, PricePoint, Utility
|
|
10
11
|
from .tariff.retail import RetailTariff
|
|
11
12
|
from .timeutil import PACIFIC, hour_floor, now_pacific, to_pacific
|
|
12
13
|
|
|
@@ -21,6 +22,8 @@ class RateEngine:
|
|
|
21
22
|
|
|
22
23
|
def __init__(self, config: Config | None = None) -> None:
|
|
23
24
|
self.config = config or Config()
|
|
25
|
+
if self.config.utility is not Utility.PACIFIC_GAS_AND_ELECTRIC:
|
|
26
|
+
raise DataError(f"{self.config.utility.display_name} pricing is not supported")
|
|
24
27
|
self.tariff = RetailTariff(self.config)
|
|
25
28
|
self.export_rates = NbtExportRates(self.config)
|
|
26
29
|
|
|
@@ -69,7 +72,7 @@ class RateEngine:
|
|
|
69
72
|
snapshot = self.tariff.snapshot_for(when)
|
|
70
73
|
low, high = self.export_rates.covered_years
|
|
71
74
|
return {
|
|
72
|
-
"utility": self.config.utility,
|
|
75
|
+
"utility": self.config.utility.value,
|
|
73
76
|
"tariff": self.config.tariff,
|
|
74
77
|
"supplier": str(self.config.supplier),
|
|
75
78
|
"tariff_effective": snapshot.effective.isoformat(),
|
|
@@ -78,5 +81,6 @@ class RateEngine:
|
|
|
78
81
|
"export_vintage": self.export_rates.vintage,
|
|
79
82
|
"export_years": [low, high],
|
|
80
83
|
"acc_plus": self.export_rates.acc_plus,
|
|
84
|
+
"pto_date": self.config.pto_date.isoformat() if self.config.pto_date else None,
|
|
81
85
|
"lock_end": self.config.lock_end.isoformat() if self.config.lock_end else None,
|
|
82
86
|
}
|
|
@@ -15,7 +15,7 @@ from typing import Any
|
|
|
15
15
|
from ..config import FLOATING_VINTAGE, Config
|
|
16
16
|
from ..data import read_data_json_gz, versioned
|
|
17
17
|
from ..errors import ConfigError, DataError, OutOfRangeError
|
|
18
|
-
from ..models import ExportPrice, Supplier
|
|
18
|
+
from ..models import ExportPrice, Supplier, Utility
|
|
19
19
|
from ..timeutil import MONTHS, DayType, day_type, export_hour
|
|
20
20
|
|
|
21
21
|
#: Where a utility's ACC Plus adder table lives. Keyed by utility rather than
|
|
@@ -32,9 +32,10 @@ def _matrix(vintage: str) -> dict[str, Any]:
|
|
|
32
32
|
return payload
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
def _acc_plus_table(utility: str, on: date) -> dict[str, Any]:
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
def _acc_plus_table(utility: Utility | str, on: date) -> dict[str, Any]:
|
|
36
|
+
utility_id = Utility(utility)
|
|
37
|
+
relative = ACC_PLUS_DIR.format(utility=utility_id.data_slug)
|
|
38
|
+
return versioned.load(relative, on, label=f"{utility_id.short_name} ACC Plus").raw
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
class NbtExportRates:
|
|
@@ -28,6 +28,40 @@ class Supplier(StrEnum):
|
|
|
28
28
|
CCA = "cca"
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
class Utility(StrEnum):
|
|
32
|
+
"""Stable utility identities used by profiles and API payloads."""
|
|
33
|
+
|
|
34
|
+
PACIFIC_GAS_AND_ELECTRIC = "pacific_gas_and_electric"
|
|
35
|
+
PORTLAND_GENERAL_ELECTRIC = "pge"
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def data_slug(self) -> str:
|
|
39
|
+
"""Private filesystem key for this utility's vendored data."""
|
|
40
|
+
match self:
|
|
41
|
+
case Utility.PACIFIC_GAS_AND_ELECTRIC:
|
|
42
|
+
return "pge"
|
|
43
|
+
case Utility.PORTLAND_GENERAL_ELECTRIC:
|
|
44
|
+
return "portland_general_electric"
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def short_name(self) -> str:
|
|
48
|
+
"""Recognizable user-facing abbreviation."""
|
|
49
|
+
match self:
|
|
50
|
+
case Utility.PACIFIC_GAS_AND_ELECTRIC:
|
|
51
|
+
return "PG&E"
|
|
52
|
+
case Utility.PORTLAND_GENERAL_ELECTRIC:
|
|
53
|
+
return "PGE"
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def display_name(self) -> str:
|
|
57
|
+
"""Full user-facing company name."""
|
|
58
|
+
match self:
|
|
59
|
+
case Utility.PACIFIC_GAS_AND_ELECTRIC:
|
|
60
|
+
return "Pacific Gas and Electric Company"
|
|
61
|
+
case Utility.PORTLAND_GENERAL_ELECTRIC:
|
|
62
|
+
return "Portland General Electric"
|
|
63
|
+
|
|
64
|
+
|
|
31
65
|
@dataclass(frozen=True, slots=True)
|
|
32
66
|
class ImportPrice:
|
|
33
67
|
"""What a kWh drawn from the grid costs, marginally."""
|