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.
Files changed (109) hide show
  1. {tariffkit-0.2.1 → tariffkit-0.2.3}/CHANGELOG.md +59 -1
  2. {tariffkit-0.2.1 → tariffkit-0.2.3}/PKG-INFO +25 -4
  3. {tariffkit-0.2.1 → tariffkit-0.2.3}/README.md +23 -2
  4. {tariffkit-0.2.1 → tariffkit-0.2.3}/pyproject.toml +6 -2
  5. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/__init__.py +2 -0
  6. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/cca.py +7 -1
  7. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/cli.py +7 -0
  8. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/config.py +4 -3
  9. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/cca/mce/2023-01-01.toml +1 -1
  10. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/cca/mce/2026-04-01.toml +1 -1
  11. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/export/pge/acc_plus/2023-04-15.toml +2 -6
  12. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/nsc/pge.toml +2 -2
  13. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/eelec/2025-01-01.toml +1 -1
  14. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/eelec/2025-03-01.toml +1 -1
  15. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/eelec/2025-09-01.toml +1 -1
  16. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/eelec/2026-01-01.toml +1 -1
  17. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/eelec/2026-03-01.toml +2 -2
  18. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/etouc/2025-01-01.toml +1 -1
  19. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/etouc/2025-03-01.toml +1 -1
  20. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/etouc/2025-09-01.toml +1 -1
  21. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/etouc/2026-01-01.toml +1 -1
  22. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/etouc/2026-03-01.toml +6 -4
  23. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/ev2a/2025-01-01.toml +1 -1
  24. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/ev2a/2025-03-01.toml +1 -1
  25. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/ev2a/2025-09-01.toml +1 -1
  26. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/ev2a/2026-01-01.toml +1 -1
  27. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/ev2a/2026-03-01.toml +2 -2
  28. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/engine.py +6 -2
  29. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/export/nbt.py +5 -4
  30. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/models.py +34 -0
  31. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/mqtt/discovery.py +5 -2
  32. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/mqtt/publisher.py +29 -1
  33. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/tariff/retail.py +11 -6
  34. {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_account_cli.py +12 -1
  35. {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_eelec.py +10 -6
  36. {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_engine.py +26 -2
  37. {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_integrations.py +4 -0
  38. {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_mce.py +6 -2
  39. {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_mqtt_publisher.py +92 -0
  40. {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_private_config.py +4 -1
  41. {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_schedules.py +12 -7
  42. {tariffkit-0.2.1 → tariffkit-0.2.3}/.gitignore +0 -0
  43. {tariffkit-0.2.1 → tariffkit-0.2.3}/LICENSE +0 -0
  44. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/account/__init__.py +0 -0
  45. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/account/cli.py +0 -0
  46. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/account/errors.py +0 -0
  47. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/account/model.py +0 -0
  48. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/account/rates.py +0 -0
  49. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/account/repository.py +0 -0
  50. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/billing/__init__.py +0 -0
  51. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/billing/engine.py +0 -0
  52. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/billing/ledger.py +0 -0
  53. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/billing/models.py +0 -0
  54. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/billing/netting.py +0 -0
  55. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/billing/trueup.py +0 -0
  56. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/__init__.py +0 -0
  57. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/export/pge/nbt00.json.gz +0 -0
  58. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/export/pge/nbt23.json.gz +0 -0
  59. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/export/pge/nbt24.json.gz +0 -0
  60. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/export/pge/nbt25.json.gz +0 -0
  61. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/export/pge/nbt26.json.gz +0 -0
  62. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/holidays.toml +0 -0
  63. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/manifest.json +0 -0
  64. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tax/ca_energy_resources/2025-01-01.toml +0 -0
  65. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/tax/ca_energy_resources/2026-01-01.toml +0 -0
  66. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/data/versioned.py +0 -0
  67. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/errors.py +0 -0
  68. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/export/__init__.py +0 -0
  69. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/interop/__init__.py +0 -0
  70. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/interop/emhass.py +0 -0
  71. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/interop/predbat.py +0 -0
  72. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/interop/slots.py +0 -0
  73. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/mqtt/__init__.py +0 -0
  74. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/providers/__init__.py +0 -0
  75. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/providers/pge/__init__.py +0 -0
  76. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/providers/pge/reconcile.py +0 -0
  77. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/providers/pge/statements/__init__.py +0 -0
  78. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/providers/pge/statements/errors.py +0 -0
  79. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/providers/pge/statements/model.py +0 -0
  80. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/providers/pge/statements/ocr.py +0 -0
  81. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/providers/pge/statements/parse.py +0 -0
  82. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/py.typed +0 -0
  83. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/secrets.py +0 -0
  84. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/sources/__init__.py +0 -0
  85. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/sources/greenbutton.py +0 -0
  86. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/sources/homeassistant.py +0 -0
  87. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/sources/influx.py +0 -0
  88. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/sources/pge.py +0 -0
  89. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/tariff/__init__.py +0 -0
  90. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/timeutil.py +0 -0
  91. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/web/__init__.py +0 -0
  92. {tariffkit-0.2.1 → tariffkit-0.2.3}/src/tariffkit/web/app.py +0 -0
  93. {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/conftest.py +0 -0
  94. {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/fixtures/nbt_golden_rows.jsonl +0 -0
  95. {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/fixtures/statements/synthetic_cca_ratechange.txt +0 -0
  96. {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_account.py +0 -0
  97. {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_billing.py +0 -0
  98. {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_ev2a.py +0 -0
  99. {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_export_golden.py +0 -0
  100. {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_interop.py +0 -0
  101. {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_ledger.py +0 -0
  102. {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_pge_reconcile.py +0 -0
  103. {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_pge_statements.py +0 -0
  104. {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_sources_greenbutton.py +0 -0
  105. {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_sources_ha.py +0 -0
  106. {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_sources_influx.py +0 -0
  107. {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_sources_pge.py +0 -0
  108. {tariffkit-0.2.1 → tariffkit-0.2.3}/tests/test_trueup.py +0 -0
  109. {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.1...HEAD
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.1
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
+ [![CI](https://github.com/eman/tariffkit/actions/workflows/ci.yml/badge.svg)](https://github.com/eman/tariffkit/actions/workflows/ci.yml)
53
+ [![PyPI](https://img.shields.io/pypi/v/tariffkit)](https://pypi.org/project/tariffkit/)
54
+ [![Python 3.14.2](https://img.shields.io/badge/python-3.14.2-blue.svg)](https://www.python.org/downloads/)
55
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](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 custom component is also available through HACS as a custom
138
- repository while its default-store submission is reviewed:
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
  [![Open your Home Assistant instance and add the TariffKit repository to HACS](https://my.home-assistant.io/badges/hacs_repository.svg)](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
+ [![CI](https://github.com/eman/tariffkit/actions/workflows/ci.yml/badge.svg)](https://github.com/eman/tariffkit/actions/workflows/ci.yml)
8
+ [![PyPI](https://img.shields.io/pypi/v/tariffkit)](https://pypi.org/project/tariffkit/)
9
+ [![Python 3.14.2](https://img.shields.io/badge/python-3.14.2-blue.svg)](https://www.python.org/downloads/)
10
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](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 custom component is also available through HACS as a custom
93
- repository while its default-store submission is reviewed:
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
  [![Open your Home Assistant instance and add the TariffKit repository to HACS](https://my.home-assistant.io/badges/hacs_repository.svg)](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.1"
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
- return CcaRateCard(provider=str(version.raw["provider"]), raw=version.raw)
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: str = "PGE"
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,
@@ -16,7 +16,7 @@
16
16
  schema = 1
17
17
  provider = "MCE"
18
18
  name = "Marin Clean Energy"
19
- utility = "PGE"
19
+ utility = "pacific_gas_and_electric"
20
20
  schedules = ["E-ELEC", "E-TOU-C", "EV2-A"]
21
21
  effective = "2023-01-01"
22
22
  currency = "USD/kWh"
@@ -49,7 +49,7 @@
49
49
  schema = 1
50
50
  provider = "MCE"
51
51
  name = "Marin Clean Energy"
52
- utility = "PGE"
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 = "PGE"
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"
@@ -18,7 +18,7 @@
18
18
  # makes a silent mis-parse essentially impossible.
19
19
 
20
20
  schema = 1
21
- utility = "PGE"
21
+ utility = "pacific_gas_and_electric"
22
22
  tariff = "E-ELEC"
23
23
  effective = "2025-01-01"
24
24
  advice_letter = "7469-E"
@@ -17,7 +17,7 @@
17
17
  # makes a silent mis-parse essentially impossible.
18
18
 
19
19
  schema = 1
20
- utility = "PGE"
20
+ utility = "pacific_gas_and_electric"
21
21
  tariff = "E-ELEC"
22
22
  effective = "2025-03-01"
23
23
  advice_letter = "7516-E"
@@ -17,7 +17,7 @@
17
17
  # makes a silent mis-parse essentially impossible.
18
18
 
19
19
  schema = 1
20
- utility = "PGE"
20
+ utility = "pacific_gas_and_electric"
21
21
  tariff = "E-ELEC"
22
22
  effective = "2025-09-01"
23
23
  advice_letter = "7684-E"
@@ -18,7 +18,7 @@
18
18
  # makes a silent mis-parse essentially impossible.
19
19
 
20
20
  schema = 1
21
- utility = "PGE"
21
+ utility = "pacific_gas_and_electric"
22
22
  tariff = "E-ELEC"
23
23
  effective = "2026-01-01"
24
24
  advice_letter = "7797-E"
@@ -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 = "PGE"
25
+ utility = "pacific_gas_and_electric"
26
26
  tariff = "E-ELEC"
27
27
  effective = "2026-03-01"
28
28
  advice_letter = "7846-E"
@@ -19,7 +19,7 @@
19
19
  # makes a silent mis-parse essentially impossible.
20
20
 
21
21
  schema = 1
22
- utility = "PGE"
22
+ utility = "pacific_gas_and_electric"
23
23
  tariff = "E-TOU-C"
24
24
  effective = "2025-01-01"
25
25
  advice_letter = "7469-E"
@@ -18,7 +18,7 @@
18
18
  # makes a silent mis-parse essentially impossible.
19
19
 
20
20
  schema = 1
21
- utility = "PGE"
21
+ utility = "pacific_gas_and_electric"
22
22
  tariff = "E-TOU-C"
23
23
  effective = "2025-03-01"
24
24
  advice_letter = "7516-E"
@@ -18,7 +18,7 @@
18
18
  # makes a silent mis-parse essentially impossible.
19
19
 
20
20
  schema = 1
21
- utility = "PGE"
21
+ utility = "pacific_gas_and_electric"
22
22
  tariff = "E-TOU-C"
23
23
  effective = "2025-09-01"
24
24
  advice_letter = "7684-E"
@@ -19,7 +19,7 @@
19
19
  # makes a silent mis-parse essentially impossible.
20
20
 
21
21
  schema = 1
22
- utility = "PGE"
22
+ utility = "pacific_gas_and_electric"
23
23
  tariff = "E-TOU-C"
24
24
  effective = "2026-01-01"
25
25
  advice_letter = "7797-E"
@@ -1,4 +1,4 @@
1
- # PG&E Schedule E-TOU-C -- retail time-of-use rates.
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 = "PGE"
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
- # Schedule E-FFS franchise fee surcharge. A different schedule, so it is
197
- # carried forward rather than read from this sheet.
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
@@ -18,7 +18,7 @@
18
18
  # makes a silent mis-parse essentially impossible.
19
19
 
20
20
  schema = 1
21
- utility = "PGE"
21
+ utility = "pacific_gas_and_electric"
22
22
  tariff = "EV2-A"
23
23
  effective = "2025-01-01"
24
24
  advice_letter = "7469-E"
@@ -17,7 +17,7 @@
17
17
  # makes a silent mis-parse essentially impossible.
18
18
 
19
19
  schema = 1
20
- utility = "PGE"
20
+ utility = "pacific_gas_and_electric"
21
21
  tariff = "EV2-A"
22
22
  effective = "2025-03-01"
23
23
  advice_letter = "7516-E"
@@ -17,7 +17,7 @@
17
17
  # makes a silent mis-parse essentially impossible.
18
18
 
19
19
  schema = 1
20
- utility = "PGE"
20
+ utility = "pacific_gas_and_electric"
21
21
  tariff = "EV2-A"
22
22
  effective = "2025-09-01"
23
23
  advice_letter = "7684-E"
@@ -18,7 +18,7 @@
18
18
  # makes a silent mis-parse essentially impossible.
19
19
 
20
20
  schema = 1
21
- utility = "PGE"
21
+ utility = "pacific_gas_and_electric"
22
22
  tariff = "EV2-A"
23
23
  effective = "2026-01-01"
24
24
  advice_letter = "7797-E"
@@ -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 = "PGE"
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
- relative = ACC_PLUS_DIR.format(utility=utility.lower())
37
- return versioned.load(relative, on, label=f"{utility} ACC Plus").raw
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."""