tariffkit 0.2.3__tar.gz → 0.3.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.
- {tariffkit-0.2.3 → tariffkit-0.3.0}/CHANGELOG.md +45 -1
- {tariffkit-0.2.3 → tariffkit-0.3.0}/PKG-INFO +11 -7
- {tariffkit-0.2.3 → tariffkit-0.3.0}/README.md +10 -6
- {tariffkit-0.2.3 → tariffkit-0.3.0}/pyproject.toml +1 -1
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/__init__.py +2 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/billing/engine.py +26 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/cca.py +5 -0
- tariffkit-0.3.0/src/tariffkit/components.py +200 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/config.py +62 -0
- tariffkit-0.3.0/src/tariffkit/data/tariff/pge/e1/2026-03-01.toml +224 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/tariff/pge/etouc/2025-01-01.toml +5 -1
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/tariff/pge/etouc/2025-03-01.toml +5 -1
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/tariff/pge/etouc/2025-09-01.toml +5 -1
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/tariff/pge/etouc/2026-01-01.toml +5 -1
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/tariff/pge/etouc/2026-03-01.toml +5 -1
- tariffkit-0.3.0/src/tariffkit/data/tariff/pge/etoud/2026-03-01.toml +143 -0
- tariffkit-0.3.0/src/tariffkit/data/tariff/pge/program/dcare/2025-01-01.toml +14 -0
- tariffkit-0.3.0/src/tariffkit/data/tariff/pge/program/dmedical/2026-03-01.toml +14 -0
- tariffkit-0.3.0/src/tariffkit/data/tariff/pge/program/ersmart/2022-05-01.toml +19 -0
- tariffkit-0.3.0/src/tariffkit/data/tariff/pge/program/medicalbaseline/2023-03-23.toml +13 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/engine.py +10 -1
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/interop/predbat.py +17 -11
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/models.py +20 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/mqtt/discovery.py +53 -1
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/mqtt/publisher.py +40 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/providers/pge/reconcile.py +2 -1
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/providers/pge/statements/parse.py +3 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/tariff/retail.py +84 -31
- {tariffkit-0.2.3 → tariffkit-0.3.0}/tests/test_billing.py +24 -0
- tariffkit-0.3.0/tests/test_components.py +164 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/tests/test_eelec.py +9 -4
- {tariffkit-0.2.3 → tariffkit-0.3.0}/tests/test_engine.py +48 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/tests/test_integrations.py +25 -2
- {tariffkit-0.2.3 → tariffkit-0.3.0}/tests/test_interop.py +39 -14
- {tariffkit-0.2.3 → tariffkit-0.3.0}/tests/test_mqtt_publisher.py +54 -5
- {tariffkit-0.2.3 → tariffkit-0.3.0}/tests/test_schedules.py +155 -16
- {tariffkit-0.2.3 → tariffkit-0.3.0}/.gitignore +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/LICENSE +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/account/__init__.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/account/cli.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/account/errors.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/account/model.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/account/rates.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/account/repository.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/billing/__init__.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/billing/ledger.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/billing/models.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/billing/netting.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/billing/trueup.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/cli.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/__init__.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/cca/mce/2023-01-01.toml +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/cca/mce/2026-04-01.toml +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/export/pge/acc_plus/2023-04-15.toml +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/export/pge/nbt00.json.gz +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/export/pge/nbt23.json.gz +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/export/pge/nbt24.json.gz +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/export/pge/nbt25.json.gz +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/export/pge/nbt26.json.gz +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/holidays.toml +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/manifest.json +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/nsc/pge.toml +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/tariff/pge/eelec/2025-01-01.toml +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/tariff/pge/eelec/2025-03-01.toml +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/tariff/pge/eelec/2025-09-01.toml +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/tariff/pge/eelec/2026-01-01.toml +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/tariff/pge/eelec/2026-03-01.toml +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/tariff/pge/ev2a/2025-01-01.toml +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/tariff/pge/ev2a/2025-03-01.toml +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/tariff/pge/ev2a/2025-09-01.toml +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/tariff/pge/ev2a/2026-01-01.toml +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/tariff/pge/ev2a/2026-03-01.toml +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/tax/ca_energy_resources/2025-01-01.toml +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/tax/ca_energy_resources/2026-01-01.toml +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/data/versioned.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/errors.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/export/__init__.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/export/nbt.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/interop/__init__.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/interop/emhass.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/interop/slots.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/mqtt/__init__.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/providers/__init__.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/providers/pge/__init__.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/providers/pge/statements/__init__.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/providers/pge/statements/errors.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/providers/pge/statements/model.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/providers/pge/statements/ocr.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/py.typed +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/secrets.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/sources/__init__.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/sources/greenbutton.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/sources/homeassistant.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/sources/influx.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/sources/pge.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/tariff/__init__.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/timeutil.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/web/__init__.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/src/tariffkit/web/app.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/tests/conftest.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/tests/fixtures/nbt_golden_rows.jsonl +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/tests/fixtures/statements/synthetic_cca_ratechange.txt +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/tests/test_account.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/tests/test_account_cli.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/tests/test_ev2a.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/tests/test_export_golden.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/tests/test_ledger.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/tests/test_mce.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/tests/test_pge_reconcile.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/tests/test_pge_statements.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/tests/test_private_config.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/tests/test_sources_greenbutton.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/tests/test_sources_ha.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/tests/test_sources_influx.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/tests/test_sources_pge.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/tests/test_trueup.py +0 -0
- {tariffkit-0.2.3 → tariffkit-0.3.0}/tests/test_versioned.py +0 -0
|
@@ -5,6 +5,49 @@ All notable changes to this project are documented here. This project follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.3.0] - 2026-08-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- PG&E's complete active single-family residential lineup is now covered:
|
|
12
|
+
generated E-1 and E-TOU-D snapshots join E-ELEC, E-TOU-C, and EV2-A.
|
|
13
|
+
E-1 preserves tiered baseline billing while exposing the over-baseline
|
|
14
|
+
marginal price, and E-TOU-D observes its weekday-only 5–8 p.m. peak and
|
|
15
|
+
tariff holiday calendar.
|
|
16
|
+
- Generated, effective-dated D-CARE, D-MEDICAL, Rule 19 Medical Baseline, and
|
|
17
|
+
E-RSMART data now drive residential program adjustments. SmartRate accepts
|
|
18
|
+
explicit announced event dates and marks prices beyond the authoritative
|
|
19
|
+
event horizon incomplete rather than guessing future events.
|
|
20
|
+
- Prices now decompose into a fixed set of chartable component groups —
|
|
21
|
+
generation, distribution, transmission, surcharges, credits, and a catch-all
|
|
22
|
+
other on the import side; generation, delivery, credits, and other on the
|
|
23
|
+
export side. The groups sum back to the price they came from and do not vary
|
|
24
|
+
with the tariff, supplier, or discount, so a chart built against them
|
|
25
|
+
survives an account change. `ImportPrice.grouped()`, `ExportPrice.grouped()`,
|
|
26
|
+
a `groups` key in every `to_dict` payload, and `tariffkit.components` expose
|
|
27
|
+
them to library and REST callers.
|
|
28
|
+
- Home Assistant gains a sensor per component group in each direction, and the
|
|
29
|
+
forecast's `rates` attribute carries the same roll-up per hour, so both the
|
|
30
|
+
recorded past and the next 48 hours can be drawn as stacked charts -- one
|
|
31
|
+
card each, because stacking recorded and forecast points together would
|
|
32
|
+
double-count the current hour. The MQTT publisher publishes the same series
|
|
33
|
+
with matching discovery payloads, each band carrying its price's quality
|
|
34
|
+
flags.
|
|
35
|
+
- Home Assistant now exposes the AB 205 Base Services Charge as **Daily Fixed
|
|
36
|
+
Charge** in `USD/day`. The unit keeps it out of the Energy dashboard's price
|
|
37
|
+
pickers and out of any `USD/kWh` stack, which is why it can be published at
|
|
38
|
+
all: it is a fixed daily amount, not a marginal price.
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
- Home Assistant now offers every active PG&E residential schedule and
|
|
42
|
+
Medical Baseline configuration while preserving existing profile and entity
|
|
43
|
+
identities.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
- Predbat attributes now use its `from` / `to` / `rate` contract for values
|
|
47
|
+
already expressed in cents. Predbat no longer interprets TariffKit's cents as
|
|
48
|
+
currency units and multiplies them by 100, while Pacific-midnight anchoring
|
|
49
|
+
and complete 46-, 48-, and 50-slot tariff days remain unchanged.
|
|
50
|
+
|
|
8
51
|
## [0.2.3] - 2026-08-17
|
|
9
52
|
|
|
10
53
|
### Changed
|
|
@@ -240,7 +283,8 @@ Initial release.
|
|
|
240
283
|
- Holiday calendars are extracted per vintage from the source data rather than
|
|
241
284
|
recomputed, because the vintage files disagree in far-future years.
|
|
242
285
|
|
|
243
|
-
[Unreleased]: https://github.com/eman/tariffkit/compare/v0.
|
|
286
|
+
[Unreleased]: https://github.com/eman/tariffkit/compare/v0.3.0...HEAD
|
|
287
|
+
[0.3.0]: https://github.com/eman/tariffkit/releases/tag/v0.3.0
|
|
244
288
|
[0.2.3]: https://github.com/eman/tariffkit/releases/tag/v0.2.3
|
|
245
289
|
[0.2.2]: https://github.com/eman/tariffkit/releases/tag/v0.2.2
|
|
246
290
|
[0.2.1]: https://github.com/eman/tariffkit/releases/tag/v0.2.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tariffkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
|
@@ -59,8 +59,10 @@ integrations. The first data provider supports PG&E residential rate plans under
|
|
|
59
59
|
**NEM 3.0 / the Net Billing Tariff (NBT)**; the package identity is deliberately
|
|
60
60
|
not tied to one utility or tariff program.
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
The complete active single-family residential portfolio is vendored:
|
|
63
|
+
**E-1**, **E-ELEC**, **E-TOU-C**, **E-TOU-D**, and **EV2-A**. CARE, FERA,
|
|
64
|
+
Medical Baseline/D-MEDICAL, and event-injected SmartRate adjustments are
|
|
65
|
+
modeled separately from their underlying plans.
|
|
64
66
|
|
|
65
67
|
Under NBT your export credit is not a time-of-use schedule: it is an hourly
|
|
66
68
|
Avoided Cost Calculator value that swings from about $0.06/kWh at midday to
|
|
@@ -92,9 +94,9 @@ component. `tariffkit` collapses it at build time, verifying losslessness cell b
|
|
|
92
94
|
cell, so the entire five-vintage dataset ships inside the wheel at **268 KiB**
|
|
93
95
|
and every lookup is a few list indexes.
|
|
94
96
|
|
|
95
|
-
The retail side is similar:
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
The retail side is similar: generated effective-dated tables hold every active
|
|
98
|
+
schedule. E-TOU-D additionally selects weekday versus weekend/observed-holiday
|
|
99
|
+
periods from the vendored tariff calendar.
|
|
98
100
|
|
|
99
101
|
Nothing here touches the network at runtime.
|
|
100
102
|
|
|
@@ -108,7 +110,9 @@ Nothing here touches the network at runtime.
|
|
|
108
110
|
| [Bill calculator](docs/billing.md) | Computing a cycle from interval meter data |
|
|
109
111
|
| [MQTT](docs/mqtt.md) | Publishing, with Home Assistant discovery |
|
|
110
112
|
| [REST API](docs/web.md) | HTTP service |
|
|
111
|
-
| [Home Assistant](docs/home-assistant.md) | Custom component, Energy dashboard, account history, response actions, opt-in Predbat |
|
|
113
|
+
| [Home Assistant](docs/home-assistant.md) | Custom component, Energy dashboard, stacked component charts, account history, response actions, opt-in Predbat |
|
|
114
|
+
| [Predbat](docs/predbat.md) | Installing TariffKit and Predbat together, end to end |
|
|
115
|
+
| [Predbat on Sigenergy](docs/predbat-sigenergy.md) | Sigenergy SigenStor specifics: entity mapping, sign and unit conversions, control |
|
|
112
116
|
| [Home Assistant quality checklist](docs/home-assistant-quality.md) | Self-assessment against the Integration Quality Scale, with exemptions |
|
|
113
117
|
| [Containers](docs/containers.md) | Local Home Assistant development stack and API/MQTT deployment proposal |
|
|
114
118
|
| [Maintaining rate data](docs/data.md) | Regenerating export rates, updating the retail tariff and CCA cards |
|
|
@@ -14,8 +14,10 @@ integrations. The first data provider supports PG&E residential rate plans under
|
|
|
14
14
|
**NEM 3.0 / the Net Billing Tariff (NBT)**; the package identity is deliberately
|
|
15
15
|
not tied to one utility or tariff program.
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
The complete active single-family residential portfolio is vendored:
|
|
18
|
+
**E-1**, **E-ELEC**, **E-TOU-C**, **E-TOU-D**, and **EV2-A**. CARE, FERA,
|
|
19
|
+
Medical Baseline/D-MEDICAL, and event-injected SmartRate adjustments are
|
|
20
|
+
modeled separately from their underlying plans.
|
|
19
21
|
|
|
20
22
|
Under NBT your export credit is not a time-of-use schedule: it is an hourly
|
|
21
23
|
Avoided Cost Calculator value that swings from about $0.06/kWh at midday to
|
|
@@ -47,9 +49,9 @@ component. `tariffkit` collapses it at build time, verifying losslessness cell b
|
|
|
47
49
|
cell, so the entire five-vintage dataset ships inside the wheel at **268 KiB**
|
|
48
50
|
and every lookup is a few list indexes.
|
|
49
51
|
|
|
50
|
-
The retail side is similar:
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
The retail side is similar: generated effective-dated tables hold every active
|
|
53
|
+
schedule. E-TOU-D additionally selects weekday versus weekend/observed-holiday
|
|
54
|
+
periods from the vendored tariff calendar.
|
|
53
55
|
|
|
54
56
|
Nothing here touches the network at runtime.
|
|
55
57
|
|
|
@@ -63,7 +65,9 @@ Nothing here touches the network at runtime.
|
|
|
63
65
|
| [Bill calculator](docs/billing.md) | Computing a cycle from interval meter data |
|
|
64
66
|
| [MQTT](docs/mqtt.md) | Publishing, with Home Assistant discovery |
|
|
65
67
|
| [REST API](docs/web.md) | HTTP service |
|
|
66
|
-
| [Home Assistant](docs/home-assistant.md) | Custom component, Energy dashboard, account history, response actions, opt-in Predbat |
|
|
68
|
+
| [Home Assistant](docs/home-assistant.md) | Custom component, Energy dashboard, stacked component charts, account history, response actions, opt-in Predbat |
|
|
69
|
+
| [Predbat](docs/predbat.md) | Installing TariffKit and Predbat together, end to end |
|
|
70
|
+
| [Predbat on Sigenergy](docs/predbat-sigenergy.md) | Sigenergy SigenStor specifics: entity mapping, sign and unit conversions, control |
|
|
67
71
|
| [Home Assistant quality checklist](docs/home-assistant-quality.md) | Self-assessment against the Integration Quality Scale, with exemptions |
|
|
68
72
|
| [Containers](docs/containers.md) | Local Home Assistant development stack and API/MQTT deployment proposal |
|
|
69
73
|
| [Maintaining rate data](docs/data.md) | Regenerating export rates, updating the retail tariff and CCA cards |
|
|
@@ -14,6 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
from importlib.metadata import version
|
|
16
16
|
|
|
17
|
+
from .components import ComponentGroup
|
|
17
18
|
from .config import CcaConfig, Config
|
|
18
19
|
from .engine import RateEngine
|
|
19
20
|
from .errors import ConfigError, DataError, OutOfRangeError, TariffKitError
|
|
@@ -34,6 +35,7 @@ __version__ = version("tariffkit")
|
|
|
34
35
|
__all__ = [
|
|
35
36
|
"PACIFIC",
|
|
36
37
|
"CcaConfig",
|
|
38
|
+
"ComponentGroup",
|
|
37
39
|
"Config",
|
|
38
40
|
"ConfigError",
|
|
39
41
|
"DataError",
|
|
@@ -21,6 +21,7 @@ from ..config import Config
|
|
|
21
21
|
from ..engine import RateEngine
|
|
22
22
|
from ..errors import DataError
|
|
23
23
|
from ..models import ImportPrice, Season, TouPeriod
|
|
24
|
+
from ..tariff.retail import load_program
|
|
24
25
|
from ..timeutil import PACIFIC, hour_floor, to_pacific
|
|
25
26
|
from .models import Bill, BillingPeriod, IntervalReading, UsageBucket
|
|
26
27
|
from .netting import check_coverage
|
|
@@ -139,6 +140,9 @@ class BillEngine:
|
|
|
139
140
|
credit = self._baseline_credit(in_period, period)
|
|
140
141
|
if credit:
|
|
141
142
|
import_components["baseline_credit"] = credit
|
|
143
|
+
smart_rate_credit = self._smartrate_credit(in_period, period)
|
|
144
|
+
if smart_rate_credit:
|
|
145
|
+
import_components["smartrate_credit"] = smart_rate_credit
|
|
142
146
|
|
|
143
147
|
if uncompensated:
|
|
144
148
|
warnings.append(
|
|
@@ -300,6 +304,28 @@ class BillEngine:
|
|
|
300
304
|
break
|
|
301
305
|
return credit
|
|
302
306
|
|
|
307
|
+
def _smartrate_credit(
|
|
308
|
+
self, readings: Sequence[IntervalReading], period: BillingPeriod
|
|
309
|
+
) -> float:
|
|
310
|
+
"""Credits non-high-price imports once for each SmartDay in the cycle."""
|
|
311
|
+
config = self.rates.config
|
|
312
|
+
if not config.smartrate:
|
|
313
|
+
return 0.0
|
|
314
|
+
events = {event for event in config.smartrate_events if period.start <= event <= period.end}
|
|
315
|
+
if not events:
|
|
316
|
+
return 0.0
|
|
317
|
+
terms = load_program("ersmart", period.end)
|
|
318
|
+
start = int(terms["high_price_start"])
|
|
319
|
+
end = int(terms["high_price_end"])
|
|
320
|
+
eligible = 0.0
|
|
321
|
+
for reading in readings:
|
|
322
|
+
moment = to_pacific(reading.start)
|
|
323
|
+
if moment.date() in events and start <= moment.hour < end:
|
|
324
|
+
continue
|
|
325
|
+
eligible += reading.imported
|
|
326
|
+
credit = float(terms["program_credit"]) + float(terms["participation_credit"])
|
|
327
|
+
return -eligible * credit * len(events)
|
|
328
|
+
|
|
303
329
|
def _fixed_charges(self, period: BillingPeriod) -> dict[str, float]:
|
|
304
330
|
"""Charges billed per day rather than per kWh.
|
|
305
331
|
|
|
@@ -33,6 +33,11 @@ class CcaRateCard:
|
|
|
33
33
|
def source_url(self) -> str:
|
|
34
34
|
return str(self.raw.get("source_url", ""))
|
|
35
35
|
|
|
36
|
+
@property
|
|
37
|
+
def supported_schedules(self) -> frozenset[str]:
|
|
38
|
+
"""PG&E schedule slugs for which this card publishes generation rates."""
|
|
39
|
+
return frozenset(self.raw["generation"])
|
|
40
|
+
|
|
36
41
|
def generation(
|
|
37
42
|
self, schedule: str, season: str, period: str, option: str = "light_green"
|
|
38
43
|
) -> float:
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Rolling unbundled tariff components up into a fixed, stackable vocabulary.
|
|
2
|
+
|
|
3
|
+
A price's ``components`` mapping speaks the tariff sheet's own language: fifteen
|
|
4
|
+
or so import lines on a bundled residential schedule, a different set once a CCA
|
|
5
|
+
supplies generation, and another line again when a CARE discount or a SmartRate
|
|
6
|
+
event applies. That is exactly the right level for reconciling a statement, and
|
|
7
|
+
exactly the wrong level for a chart -- a fifteen-band stack is unreadable, and
|
|
8
|
+
a consumer that made one series per line would watch series appear and vanish as
|
|
9
|
+
the account's configuration changed.
|
|
10
|
+
|
|
11
|
+
The groups below are a small closed vocabulary that every component maps into.
|
|
12
|
+
Two properties make them safe to draw as a stacked graph against the price
|
|
13
|
+
itself:
|
|
14
|
+
|
|
15
|
+
* the set of groups for a direction is fixed, so the series exist whether or not
|
|
16
|
+
the account happens to pay that kind of charge this hour; and
|
|
17
|
+
* every component lands in exactly one group -- anything this table has not seen
|
|
18
|
+
falls into :attr:`ComponentGroup.OTHER` rather than being dropped -- so the
|
|
19
|
+
groups sum back to the price, within per-component rounding.
|
|
20
|
+
|
|
21
|
+
Grouping is a presentation, not a billing rule. ``tariffkit.billing.ledger``
|
|
22
|
+
classifies the same components again, differently and for a different purpose:
|
|
23
|
+
which export credits may offset which charges. Neither is derived from the
|
|
24
|
+
other, because being drawn in the same band of a chart and being offsettable by
|
|
25
|
+
the same credit bucket are unrelated questions.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from collections.abc import Iterable, Mapping
|
|
31
|
+
from enum import StrEnum
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ComponentGroup(StrEnum):
|
|
35
|
+
"""A band in a stacked price chart."""
|
|
36
|
+
|
|
37
|
+
#: Generation supply, whoever supplies it, plus the charges that follow the
|
|
38
|
+
#: supply decision: the PCIA a departed customer pays, the bundled PCIA
|
|
39
|
+
#: credit a PG&E-supplied one receives, a CCA's own cost relief credit, and
|
|
40
|
+
#: a SmartRate event's high-price energy charge.
|
|
41
|
+
GENERATION = "generation"
|
|
42
|
+
#: Distribution, and the Conservation Incentive Adjustment that implements
|
|
43
|
+
#: the baseline credit as a distribution-side rate spread.
|
|
44
|
+
DISTRIBUTION = "distribution"
|
|
45
|
+
#: Transmission, its rate adjustments, and reliability services.
|
|
46
|
+
TRANSMISSION = "transmission"
|
|
47
|
+
#: The export side's delivery component. PG&E publishes the export rate as
|
|
48
|
+
#: exactly two components -- delivery, which every Solar Billing Plan
|
|
49
|
+
#: customer earns, and generation, which only a bundled one does. Delivery
|
|
50
|
+
#: is not published split into distribution and transmission, so it gets its
|
|
51
|
+
#: own band rather than being apportioned across two on a guess.
|
|
52
|
+
DELIVERY = "delivery"
|
|
53
|
+
#: Non-bypassable charges and riders: public purpose programs, nuclear
|
|
54
|
+
#: decommissioning, competition transition, energy cost recovery, the
|
|
55
|
+
#: wildfire fund and hardening charges, new system generation, the recovery
|
|
56
|
+
#: bond charge and its offsetting credit, and the CCA franchise fee.
|
|
57
|
+
SURCHARGES = "surcharges"
|
|
58
|
+
#: Discounts and incentives: CARE, FERA, Medical Baseline, and the ACC Plus
|
|
59
|
+
#: adder on the export side.
|
|
60
|
+
CREDITS = "credits"
|
|
61
|
+
#: Anything this module has not classified. Normally zero; non-zero means a
|
|
62
|
+
#: schedule grew a line and this table has not caught up.
|
|
63
|
+
OTHER = "other"
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def label(self) -> str:
|
|
67
|
+
"""Human-readable name, for interfaces without their own translations."""
|
|
68
|
+
match self:
|
|
69
|
+
case ComponentGroup.GENERATION:
|
|
70
|
+
return "Generation"
|
|
71
|
+
case ComponentGroup.DISTRIBUTION:
|
|
72
|
+
return "Distribution"
|
|
73
|
+
case ComponentGroup.TRANSMISSION:
|
|
74
|
+
return "Transmission"
|
|
75
|
+
case ComponentGroup.DELIVERY:
|
|
76
|
+
return "Delivery"
|
|
77
|
+
case ComponentGroup.SURCHARGES:
|
|
78
|
+
return "Surcharges"
|
|
79
|
+
case ComponentGroup.CREDITS:
|
|
80
|
+
return "Credits"
|
|
81
|
+
case ComponentGroup.OTHER:
|
|
82
|
+
return "Other"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
#: The groups an import price is drawn from, in stack order: supply at the
|
|
86
|
+
#: bottom, then wires, then policy charges, then whatever reduces the bill.
|
|
87
|
+
IMPORT_GROUPS: tuple[ComponentGroup, ...] = (
|
|
88
|
+
ComponentGroup.GENERATION,
|
|
89
|
+
ComponentGroup.DISTRIBUTION,
|
|
90
|
+
ComponentGroup.TRANSMISSION,
|
|
91
|
+
ComponentGroup.SURCHARGES,
|
|
92
|
+
ComponentGroup.CREDITS,
|
|
93
|
+
ComponentGroup.OTHER,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
#: The groups an export credit is drawn from. The ACC has no retail wires or
|
|
97
|
+
#: policy charges in it -- there is nothing to bill a customer who is exporting.
|
|
98
|
+
EXPORT_GROUPS: tuple[ComponentGroup, ...] = (
|
|
99
|
+
ComponentGroup.GENERATION,
|
|
100
|
+
ComponentGroup.DELIVERY,
|
|
101
|
+
ComponentGroup.CREDITS,
|
|
102
|
+
ComponentGroup.OTHER,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
#: Component name -> group. Import and export names share one table because the
|
|
106
|
+
#: names that appear on both sides mean the same thing on both sides.
|
|
107
|
+
COMPONENT_GROUPS: dict[str, ComponentGroup] = {
|
|
108
|
+
# Generation supply and the charges attached to who supplies it.
|
|
109
|
+
"generation": ComponentGroup.GENERATION,
|
|
110
|
+
"cca_generation": ComponentGroup.GENERATION,
|
|
111
|
+
"cca_cost_relief_credit": ComponentGroup.GENERATION,
|
|
112
|
+
"cca_solar_bonus": ComponentGroup.GENERATION,
|
|
113
|
+
"bundled_pcia": ComponentGroup.GENERATION,
|
|
114
|
+
"pcia": ComponentGroup.GENERATION,
|
|
115
|
+
"smartrate_high_price": ComponentGroup.GENERATION,
|
|
116
|
+
# Distribution.
|
|
117
|
+
"distribution": ComponentGroup.DISTRIBUTION,
|
|
118
|
+
"conservation_incentive_adjustment": ComponentGroup.DISTRIBUTION,
|
|
119
|
+
# Transmission.
|
|
120
|
+
"transmission": ComponentGroup.TRANSMISSION,
|
|
121
|
+
"transmission_rate_adjustments": ComponentGroup.TRANSMISSION,
|
|
122
|
+
"reliability_services": ComponentGroup.TRANSMISSION,
|
|
123
|
+
# Export-side avoided delivery.
|
|
124
|
+
"delivery": ComponentGroup.DELIVERY,
|
|
125
|
+
# Non-bypassable charges and riders.
|
|
126
|
+
"public_purpose_programs": ComponentGroup.SURCHARGES,
|
|
127
|
+
"nuclear_decommissioning": ComponentGroup.SURCHARGES,
|
|
128
|
+
"competition_transition_charges": ComponentGroup.SURCHARGES,
|
|
129
|
+
"energy_cost_recovery": ComponentGroup.SURCHARGES,
|
|
130
|
+
"wildfire_fund_charge": ComponentGroup.SURCHARGES,
|
|
131
|
+
"wildfire_hardening": ComponentGroup.SURCHARGES,
|
|
132
|
+
"new_system_generation": ComponentGroup.SURCHARGES,
|
|
133
|
+
"recovery_bond_charge": ComponentGroup.SURCHARGES,
|
|
134
|
+
# Paired with the charge above, which it currently offsets exactly. Keeping
|
|
135
|
+
# the pair in one group means the band shows the net, rather than inflating
|
|
136
|
+
# surcharges and inventing an equal and opposite credit.
|
|
137
|
+
"recovery_bond_credit": ComponentGroup.SURCHARGES,
|
|
138
|
+
"franchise_fee_surcharge": ComponentGroup.SURCHARGES,
|
|
139
|
+
# Discounts and incentives.
|
|
140
|
+
"medical_discount": ComponentGroup.CREDITS,
|
|
141
|
+
"acc_plus": ComponentGroup.CREDITS,
|
|
142
|
+
# Bill-level components. A marginal price never carries these -- a baseline
|
|
143
|
+
# credit depends on cumulative usage, and a tax on a whole cycle's charges
|
|
144
|
+
# -- but ``group_of`` is public, so a caller grouping a ``Bill``'s
|
|
145
|
+
# breakdown gets the same answers rather than a pile of ``OTHER``. The Base
|
|
146
|
+
# Services Charge is deliberately absent: it is a $/day amount, and there
|
|
147
|
+
# is no band of a per-kWh chart it honestly belongs in.
|
|
148
|
+
"energy_commission_tax": ComponentGroup.SURCHARGES,
|
|
149
|
+
"baseline_credit": ComponentGroup.DISTRIBUTION,
|
|
150
|
+
"smartrate_credit": ComponentGroup.GENERATION,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#: The discount component is named for the program applied (``care_discount``,
|
|
154
|
+
#: ``fera_discount``), so the suffix is matched rather than every program being
|
|
155
|
+
#: enumerated -- a new discount program should not silently land in ``OTHER``.
|
|
156
|
+
_DISCOUNT_SUFFIX = "_discount"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def group_of(name: str) -> ComponentGroup:
|
|
160
|
+
"""The group ``name`` belongs to, or ``OTHER`` if it is unrecognized."""
|
|
161
|
+
group = COMPONENT_GROUPS.get(name)
|
|
162
|
+
if group is not None:
|
|
163
|
+
return group
|
|
164
|
+
if name.endswith(_DISCOUNT_SUFFIX):
|
|
165
|
+
return ComponentGroup.CREDITS
|
|
166
|
+
return ComponentGroup.OTHER
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def split_components(
|
|
170
|
+
components: Mapping[str, float],
|
|
171
|
+
groups: Iterable[ComponentGroup],
|
|
172
|
+
) -> dict[ComponentGroup, dict[str, float]]:
|
|
173
|
+
"""Bucket ``components`` by group, keeping each group's underlying lines.
|
|
174
|
+
|
|
175
|
+
Every group in ``groups`` is present, empty if nothing landed in it, so a
|
|
176
|
+
consumer builds the same series every time. A component whose group is not
|
|
177
|
+
among ``groups`` is folded into ``OTHER`` instead of being dropped, which
|
|
178
|
+
keeps the sum identity true even if a direction ever grows a line from a
|
|
179
|
+
band it was not expected to have.
|
|
180
|
+
"""
|
|
181
|
+
ordered = tuple(groups)
|
|
182
|
+
buckets: dict[ComponentGroup, dict[str, float]] = {group: {} for group in ordered}
|
|
183
|
+
for name, value in components.items():
|
|
184
|
+
group = group_of(name)
|
|
185
|
+
if group not in buckets:
|
|
186
|
+
group = ComponentGroup.OTHER
|
|
187
|
+
buckets.setdefault(group, {})
|
|
188
|
+
buckets[group][name] = value
|
|
189
|
+
return buckets
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def group_components(
|
|
193
|
+
components: Mapping[str, float],
|
|
194
|
+
groups: Iterable[ComponentGroup],
|
|
195
|
+
) -> dict[ComponentGroup, float]:
|
|
196
|
+
"""``components`` summed per group, one entry per group in ``groups``."""
|
|
197
|
+
return {
|
|
198
|
+
group: round(float(sum(lines.values())), 6)
|
|
199
|
+
for group, lines in split_components(components, groups).items()
|
|
200
|
+
}
|
|
@@ -32,6 +32,15 @@ LOCK_YEARS = 9
|
|
|
32
32
|
_ONE_DAY = timedelta(days=1)
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
def _env_bool(name: str, value: str) -> bool:
|
|
36
|
+
normalized = value.strip().lower()
|
|
37
|
+
if normalized in {"1", "true", "yes", "on"}:
|
|
38
|
+
return True
|
|
39
|
+
if normalized in {"0", "false", "no", "off"}:
|
|
40
|
+
return False
|
|
41
|
+
raise ConfigError(f"{name} must be true or false")
|
|
42
|
+
|
|
43
|
+
|
|
35
44
|
@dataclass(frozen=True, slots=True)
|
|
36
45
|
class CcaConfig:
|
|
37
46
|
"""Settings for customers whose generation comes from a CCA or ESP.
|
|
@@ -104,6 +113,17 @@ class Config:
|
|
|
104
113
|
#: electric. PG&E prints this as Code B or Code H; the bill's "Heat Source"
|
|
105
114
|
#: line says which.
|
|
106
115
|
baseline_code: BaselineCode = "basic"
|
|
116
|
+
#: Medical Baseline enrollment. Tiered schedules receive additional
|
|
117
|
+
#: baseline quantity; E-ELEC, E-TOU-D, and EV2-A receive D-MEDICAL.
|
|
118
|
+
medical_baseline: bool = False
|
|
119
|
+
medical_kwh_per_day: float | None = None
|
|
120
|
+
|
|
121
|
+
#: SmartRate is event-driven. Event dates and the date through which the
|
|
122
|
+
#: supplied calendar is authoritative are explicit so a missing future
|
|
123
|
+
#: event cannot be silently priced as an ordinary day.
|
|
124
|
+
smartrate: bool = False
|
|
125
|
+
smartrate_events: tuple[date, ...] = ()
|
|
126
|
+
smartrate_known_through: date | None = None
|
|
107
127
|
|
|
108
128
|
cca: CcaConfig | None = None
|
|
109
129
|
|
|
@@ -134,6 +154,18 @@ class Config:
|
|
|
134
154
|
object.__setattr__(self, "supplier", Supplier(self.supplier))
|
|
135
155
|
if self.supplier is Supplier.CCA and self.cca is None:
|
|
136
156
|
raise ConfigError("supplier='cca' requires a CcaConfig")
|
|
157
|
+
if self.smartrate and self.supplier is not Supplier.BUNDLED:
|
|
158
|
+
raise ConfigError("SmartRate is available only with bundled PG&E generation")
|
|
159
|
+
if self.smartrate_events and not self.smartrate:
|
|
160
|
+
raise ConfigError("smartrate_events requires smartrate=true")
|
|
161
|
+
if self.smartrate and self.smartrate_known_through is None:
|
|
162
|
+
raise ConfigError("smartrate=true requires smartrate_known_through")
|
|
163
|
+
if self.smartrate_known_through is not None and any(
|
|
164
|
+
event > self.smartrate_known_through for event in self.smartrate_events
|
|
165
|
+
):
|
|
166
|
+
raise ConfigError("a SmartRate event falls after smartrate_known_through")
|
|
167
|
+
if self.medical_kwh_per_day is not None and self.medical_kwh_per_day <= 0:
|
|
168
|
+
raise ConfigError("medical_kwh_per_day must be greater than zero")
|
|
137
169
|
if self.vintage is None and self.interconnection_year is None:
|
|
138
170
|
raise ConfigError("set either interconnection_year or vintage")
|
|
139
171
|
if self.discount != "none" and self.acc_plus_segment == "residential":
|
|
@@ -189,6 +221,15 @@ class Config:
|
|
|
189
221
|
value = data.get("pto_date")
|
|
190
222
|
if isinstance(value, str):
|
|
191
223
|
data["pto_date"] = date.fromisoformat(value)
|
|
224
|
+
value = data.get("smartrate_known_through")
|
|
225
|
+
if isinstance(value, str):
|
|
226
|
+
data["smartrate_known_through"] = date.fromisoformat(value)
|
|
227
|
+
events = data.get("smartrate_events")
|
|
228
|
+
if isinstance(events, list | tuple):
|
|
229
|
+
data["smartrate_events"] = tuple(
|
|
230
|
+
date.fromisoformat(value) if isinstance(value, str) else value
|
|
231
|
+
for value in events
|
|
232
|
+
)
|
|
192
233
|
return cls(**data)
|
|
193
234
|
except (TypeError, ValueError) as exc:
|
|
194
235
|
raise ConfigError(f"invalid config: {exc}") from exc
|
|
@@ -207,6 +248,15 @@ class Config:
|
|
|
207
248
|
"base_services_charge_tier": self.base_services_charge_tier,
|
|
208
249
|
"baseline_territory": self.baseline_territory,
|
|
209
250
|
"baseline_code": self.baseline_code,
|
|
251
|
+
"medical_baseline": self.medical_baseline,
|
|
252
|
+
"medical_kwh_per_day": self.medical_kwh_per_day,
|
|
253
|
+
"smartrate": self.smartrate,
|
|
254
|
+
"smartrate_events": [event.isoformat() for event in self.smartrate_events],
|
|
255
|
+
"smartrate_known_through": (
|
|
256
|
+
self.smartrate_known_through.isoformat()
|
|
257
|
+
if self.smartrate_known_through is not None
|
|
258
|
+
else None
|
|
259
|
+
),
|
|
210
260
|
"nsc_rate": self.nsc_rate,
|
|
211
261
|
}
|
|
212
262
|
if self.cca is not None:
|
|
@@ -262,6 +312,18 @@ class Config:
|
|
|
262
312
|
overrides["baseline_territory"] = value
|
|
263
313
|
if value := os.environ.get("TARIFFKIT_BASELINE_CODE"):
|
|
264
314
|
overrides["baseline_code"] = value
|
|
315
|
+
if value := os.environ.get("TARIFFKIT_MEDICAL_BASELINE"):
|
|
316
|
+
overrides["medical_baseline"] = _env_bool("TARIFFKIT_MEDICAL_BASELINE", value)
|
|
317
|
+
if value := os.environ.get("TARIFFKIT_MEDICAL_KWH_PER_DAY"):
|
|
318
|
+
overrides["medical_kwh_per_day"] = float(value)
|
|
319
|
+
if value := os.environ.get("TARIFFKIT_SMARTRATE"):
|
|
320
|
+
overrides["smartrate"] = _env_bool("TARIFFKIT_SMARTRATE", value)
|
|
321
|
+
if value := os.environ.get("TARIFFKIT_SMARTRATE_EVENTS"):
|
|
322
|
+
overrides["smartrate_events"] = tuple(
|
|
323
|
+
date.fromisoformat(item.strip()) for item in value.split(",") if item.strip()
|
|
324
|
+
)
|
|
325
|
+
if value := os.environ.get("TARIFFKIT_SMARTRATE_KNOWN_THROUGH"):
|
|
326
|
+
overrides["smartrate_known_through"] = date.fromisoformat(value)
|
|
265
327
|
if value := os.environ.get("TARIFFKIT_NSC_RATE"):
|
|
266
328
|
overrides["nsc_rate"] = float(value)
|
|
267
329
|
if value := os.environ.get("TARIFFKIT_CCA_JSON"):
|