tariffkit 0.2.2__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.2 → tariffkit-0.2.3}/CHANGELOG.md +21 -1
  2. {tariffkit-0.2.2 → tariffkit-0.2.3}/PKG-INFO +2 -2
  3. {tariffkit-0.2.2 → tariffkit-0.2.3}/README.md +1 -1
  4. {tariffkit-0.2.2 → tariffkit-0.2.3}/pyproject.toml +1 -1
  5. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/__init__.py +2 -0
  6. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/cca.py +7 -1
  7. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/config.py +4 -3
  8. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/cca/mce/2023-01-01.toml +1 -1
  9. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/cca/mce/2026-04-01.toml +1 -1
  10. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/export/pge/acc_plus/2023-04-15.toml +2 -6
  11. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/nsc/pge.toml +2 -2
  12. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/eelec/2025-01-01.toml +1 -1
  13. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/eelec/2025-03-01.toml +1 -1
  14. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/eelec/2025-09-01.toml +1 -1
  15. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/eelec/2026-01-01.toml +1 -1
  16. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/eelec/2026-03-01.toml +2 -2
  17. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/etouc/2025-01-01.toml +1 -1
  18. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/etouc/2025-03-01.toml +1 -1
  19. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/etouc/2025-09-01.toml +1 -1
  20. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/etouc/2026-01-01.toml +1 -1
  21. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/etouc/2026-03-01.toml +6 -4
  22. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/ev2a/2025-01-01.toml +1 -1
  23. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/ev2a/2025-03-01.toml +1 -1
  24. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/ev2a/2025-09-01.toml +1 -1
  25. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/ev2a/2026-01-01.toml +1 -1
  26. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/tariff/pge/ev2a/2026-03-01.toml +2 -2
  27. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/engine.py +6 -2
  28. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/export/nbt.py +5 -4
  29. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/models.py +34 -0
  30. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/mqtt/discovery.py +5 -2
  31. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/tariff/retail.py +11 -6
  32. {tariffkit-0.2.2 → tariffkit-0.2.3}/tests/test_eelec.py +10 -6
  33. {tariffkit-0.2.2 → tariffkit-0.2.3}/tests/test_engine.py +26 -2
  34. {tariffkit-0.2.2 → tariffkit-0.2.3}/tests/test_integrations.py +4 -0
  35. {tariffkit-0.2.2 → tariffkit-0.2.3}/tests/test_mce.py +6 -2
  36. {tariffkit-0.2.2 → tariffkit-0.2.3}/tests/test_schedules.py +12 -7
  37. {tariffkit-0.2.2 → tariffkit-0.2.3}/.gitignore +0 -0
  38. {tariffkit-0.2.2 → tariffkit-0.2.3}/LICENSE +0 -0
  39. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/account/__init__.py +0 -0
  40. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/account/cli.py +0 -0
  41. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/account/errors.py +0 -0
  42. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/account/model.py +0 -0
  43. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/account/rates.py +0 -0
  44. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/account/repository.py +0 -0
  45. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/billing/__init__.py +0 -0
  46. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/billing/engine.py +0 -0
  47. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/billing/ledger.py +0 -0
  48. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/billing/models.py +0 -0
  49. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/billing/netting.py +0 -0
  50. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/billing/trueup.py +0 -0
  51. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/cli.py +0 -0
  52. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/__init__.py +0 -0
  53. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/export/pge/nbt00.json.gz +0 -0
  54. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/export/pge/nbt23.json.gz +0 -0
  55. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/export/pge/nbt24.json.gz +0 -0
  56. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/export/pge/nbt25.json.gz +0 -0
  57. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/export/pge/nbt26.json.gz +0 -0
  58. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/holidays.toml +0 -0
  59. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/manifest.json +0 -0
  60. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/tax/ca_energy_resources/2025-01-01.toml +0 -0
  61. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/tax/ca_energy_resources/2026-01-01.toml +0 -0
  62. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/data/versioned.py +0 -0
  63. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/errors.py +0 -0
  64. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/export/__init__.py +0 -0
  65. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/interop/__init__.py +0 -0
  66. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/interop/emhass.py +0 -0
  67. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/interop/predbat.py +0 -0
  68. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/interop/slots.py +0 -0
  69. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/mqtt/__init__.py +0 -0
  70. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/mqtt/publisher.py +0 -0
  71. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/providers/__init__.py +0 -0
  72. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/providers/pge/__init__.py +0 -0
  73. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/providers/pge/reconcile.py +0 -0
  74. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/providers/pge/statements/__init__.py +0 -0
  75. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/providers/pge/statements/errors.py +0 -0
  76. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/providers/pge/statements/model.py +0 -0
  77. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/providers/pge/statements/ocr.py +0 -0
  78. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/providers/pge/statements/parse.py +0 -0
  79. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/py.typed +0 -0
  80. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/secrets.py +0 -0
  81. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/sources/__init__.py +0 -0
  82. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/sources/greenbutton.py +0 -0
  83. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/sources/homeassistant.py +0 -0
  84. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/sources/influx.py +0 -0
  85. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/sources/pge.py +0 -0
  86. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/tariff/__init__.py +0 -0
  87. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/timeutil.py +0 -0
  88. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/web/__init__.py +0 -0
  89. {tariffkit-0.2.2 → tariffkit-0.2.3}/src/tariffkit/web/app.py +0 -0
  90. {tariffkit-0.2.2 → tariffkit-0.2.3}/tests/conftest.py +0 -0
  91. {tariffkit-0.2.2 → tariffkit-0.2.3}/tests/fixtures/nbt_golden_rows.jsonl +0 -0
  92. {tariffkit-0.2.2 → tariffkit-0.2.3}/tests/fixtures/statements/synthetic_cca_ratechange.txt +0 -0
  93. {tariffkit-0.2.2 → tariffkit-0.2.3}/tests/test_account.py +0 -0
  94. {tariffkit-0.2.2 → tariffkit-0.2.3}/tests/test_account_cli.py +0 -0
  95. {tariffkit-0.2.2 → tariffkit-0.2.3}/tests/test_billing.py +0 -0
  96. {tariffkit-0.2.2 → tariffkit-0.2.3}/tests/test_ev2a.py +0 -0
  97. {tariffkit-0.2.2 → tariffkit-0.2.3}/tests/test_export_golden.py +0 -0
  98. {tariffkit-0.2.2 → tariffkit-0.2.3}/tests/test_interop.py +0 -0
  99. {tariffkit-0.2.2 → tariffkit-0.2.3}/tests/test_ledger.py +0 -0
  100. {tariffkit-0.2.2 → tariffkit-0.2.3}/tests/test_mqtt_publisher.py +0 -0
  101. {tariffkit-0.2.2 → tariffkit-0.2.3}/tests/test_pge_reconcile.py +0 -0
  102. {tariffkit-0.2.2 → tariffkit-0.2.3}/tests/test_pge_statements.py +0 -0
  103. {tariffkit-0.2.2 → tariffkit-0.2.3}/tests/test_private_config.py +0 -0
  104. {tariffkit-0.2.2 → tariffkit-0.2.3}/tests/test_sources_greenbutton.py +0 -0
  105. {tariffkit-0.2.2 → tariffkit-0.2.3}/tests/test_sources_ha.py +0 -0
  106. {tariffkit-0.2.2 → tariffkit-0.2.3}/tests/test_sources_influx.py +0 -0
  107. {tariffkit-0.2.2 → tariffkit-0.2.3}/tests/test_sources_pge.py +0 -0
  108. {tariffkit-0.2.2 → tariffkit-0.2.3}/tests/test_trueup.py +0 -0
  109. {tariffkit-0.2.2 → tariffkit-0.2.3}/tests/test_versioned.py +0 -0
@@ -5,6 +5,25 @@ 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
+
8
27
  ## [0.2.2] - 2026-08-16
9
28
 
10
29
  ### Added
@@ -221,7 +240,8 @@ Initial release.
221
240
  - Holiday calendars are extracted per vintage from the source data rather than
222
241
  recomputed, because the vintage files disagree in far-future years.
223
242
 
224
- [Unreleased]: https://github.com/eman/tariffkit/compare/v0.2.2...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
225
245
  [0.2.2]: https://github.com/eman/tariffkit/releases/tag/v0.2.2
226
246
  [0.2.1]: https://github.com/eman/tariffkit/releases/tag/v0.2.1
227
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.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
@@ -44,7 +44,7 @@ Requires-Dist: uvicorn[standard]>=0.29; extra == 'web'
44
44
  Description-Content-Type: text/markdown
45
45
 
46
46
  <p align="center">
47
- <img src="images/banner.svg" alt="TariffKit — electricity pricing, billing, and energy integrations" width="100%">
47
+ <img src="https://raw.githubusercontent.com/eman/tariffkit/main/images/banner.svg" alt="TariffKit — electricity pricing, billing, and energy integrations" width="100%">
48
48
  </p>
49
49
 
50
50
  # TariffKit
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="images/banner.svg" alt="TariffKit — electricity pricing, billing, and energy integrations" width="100%">
2
+ <img src="https://raw.githubusercontent.com/eman/tariffkit/main/images/banner.svg" alt="TariffKit — electricity pricing, billing, and energy integrations" width="100%">
3
3
  </p>
4
4
 
5
5
  # TariffKit
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "tariffkit"
7
- version = "0.2.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"
@@ -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, ...]:
@@ -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."""
@@ -8,6 +8,8 @@ from __future__ import annotations
8
8
 
9
9
  from typing import Any
10
10
 
11
+ from ..models import Utility
12
+
11
13
  DEVICE_ID = "tariffkit"
12
14
 
13
15
  #: (object_id, name, state topic suffix, icon)
@@ -19,10 +21,11 @@ SENSORS: tuple[tuple[str, str, str, str], ...] = (
19
21
 
20
22
 
21
23
  def _device(engine_info: dict[str, Any]) -> dict[str, Any]:
24
+ utility = Utility(engine_info.get("utility", Utility.PACIFIC_GAS_AND_ELECTRIC))
22
25
  return {
23
26
  "identifiers": [DEVICE_ID],
24
- "name": "PG&E Rates",
25
- "manufacturer": str(engine_info.get("utility", "PGE")),
27
+ "name": f"{utility.short_name} Rates",
28
+ "manufacturer": utility.display_name,
26
29
  "model": f"{engine_info.get('tariff', 'E-ELEC')} / {engine_info.get('export_vintage', '')}",
27
30
  "sw_version": _version(),
28
31
  }
@@ -28,7 +28,7 @@ from typing import Any
28
28
  from ..cca import load_rate_card
29
29
  from ..config import Config
30
30
  from ..errors import ConfigError, DataError
31
- from ..models import ImportPrice, Season, Supplier, TouPeriod
31
+ from ..models import ImportPrice, Season, Supplier, TouPeriod, Utility
32
32
 
33
33
  TARIFF_DATA_ROOT = "tariff"
34
34
 
@@ -50,11 +50,11 @@ class TariffSnapshot:
50
50
 
51
51
 
52
52
  @lru_cache(maxsize=8)
53
- def _snapshots(utility: str, tariff: str) -> tuple[TariffSnapshot, ...]:
53
+ def _snapshots(utility: Utility, tariff: str) -> tuple[TariffSnapshot, ...]:
54
54
  directory = (
55
55
  files("tariffkit.data")
56
56
  / TARIFF_DATA_ROOT
57
- / utility.lower()
57
+ / utility.data_slug
58
58
  / tariff.lower().replace("-", "")
59
59
  )
60
60
  if not directory.is_dir():
@@ -64,19 +64,24 @@ def _snapshots(utility: str, tariff: str) -> tuple[TariffSnapshot, ...]:
64
64
  if not entry.name.endswith(".toml"):
65
65
  continue
66
66
  raw = tomllib.loads(entry.read_text(encoding="utf-8"))
67
+ try:
68
+ raw["utility"] = Utility(raw["utility"]).value
69
+ except (KeyError, ValueError) as exc:
70
+ raise DataError(f"{entry.name}: invalid utility identifier") from exc
67
71
  found.append(TariffSnapshot(date.fromisoformat(raw["effective"]), raw))
68
72
  if not found:
69
73
  raise DataError(f"no tariff snapshots for {utility}/{tariff}")
70
74
  return tuple(sorted(found, key=lambda s: s.effective))
71
75
 
72
76
 
73
- def load_snapshot(utility: str, tariff: str, on: date) -> TariffSnapshot:
77
+ def load_snapshot(utility: Utility | str, tariff: str, on: date) -> TariffSnapshot:
74
78
  """The snapshot in force on ``on`` -- the latest one not in the future."""
75
- snapshots = _snapshots(utility, tariff)
79
+ utility_id = Utility(utility)
80
+ snapshots = _snapshots(utility_id, tariff)
76
81
  applicable = [s for s in snapshots if s.effective <= on]
77
82
  if not applicable:
78
83
  raise DataError(
79
- f"{utility}/{tariff}: no snapshot effective on or before {on}; "
84
+ f"{utility_id.value}/{tariff}: no snapshot effective on or before {on}; "
80
85
  f"earliest vendored is {snapshots[0].effective}"
81
86
  )
82
87
  return applicable[-1]
@@ -6,7 +6,7 @@ from datetime import date, datetime, timedelta
6
6
 
7
7
  import pytest
8
8
 
9
- from tariffkit import Config, Season, Supplier, TouPeriod
9
+ from tariffkit import Config, Season, Supplier, TouPeriod, Utility
10
10
  from tariffkit.config import CcaConfig
11
11
  from tariffkit.errors import ConfigError
12
12
  from tariffkit.tariff.retail import RetailTariff, load_snapshot
@@ -38,7 +38,7 @@ def test_components_sum_to_published_total(season: str, period: str, published:
38
38
 
39
39
  This is the check that catches a mistyped digit in the vendored tariff.
40
40
  """
41
- snapshot = load_snapshot("PGE", "E-ELEC", date(2026, 6, 1))
41
+ snapshot = load_snapshot(Utility.PACIFIC_GAS_AND_ELECTRIC, "E-ELEC", date(2026, 6, 1))
42
42
  energy = snapshot.raw["energy"][season][period]
43
43
  total = sum(energy.values()) + sum(snapshot.raw["adders"].values())
44
44
  assert total == pytest.approx(published, abs=5e-6)
@@ -125,10 +125,14 @@ def test_effective_dated_snapshot_selection() -> None:
125
125
  (Advice 7846-E, effective 2026-03-01), not from the later reissue of the
126
126
  totals page; see tools/regen_tariff.py::pick_effective.
127
127
  """
128
- assert load_snapshot("PGE", "E-ELEC", date(2026, 7, 1)).effective == date(2026, 3, 1)
129
- assert load_snapshot("PGE", "E-ELEC", date(2026, 4, 15)).effective == date(2026, 3, 1)
128
+ assert load_snapshot(
129
+ Utility.PACIFIC_GAS_AND_ELECTRIC, "E-ELEC", date(2026, 7, 1)
130
+ ).effective == date(2026, 3, 1)
131
+ assert load_snapshot(
132
+ Utility.PACIFIC_GAS_AND_ELECTRIC, "E-ELEC", date(2026, 4, 15)
133
+ ).effective == date(2026, 3, 1)
130
134
  with pytest.raises(Exception, match="no snapshot effective"):
131
- load_snapshot("PGE", "E-ELEC", date(2020, 1, 1))
135
+ load_snapshot(Utility.PACIFIC_GAS_AND_ELECTRIC, "E-ELEC", date(2020, 1, 1))
132
136
 
133
137
 
134
138
  def test_base_services_charge_is_daily_and_excluded_from_energy_price(
@@ -212,7 +216,7 @@ class TestCca:
212
216
  pcia_vintage is documented as resolving both; this is what makes that
213
217
  true rather than true-for-now.
214
218
  """
215
- cca = load_snapshot("PGE", "E-ELEC", date(2026, 6, 1)).raw["cca"]
219
+ cca = load_snapshot(Utility.PACIFIC_GAS_AND_ELECTRIC, "E-ELEC", date(2026, 6, 1)).raw["cca"]
216
220
  assert set(cca["pcia_vintages"]) == set(cca["franchise_fee_vintages"])
217
221
 
218
222
  @pytest.mark.parametrize(
@@ -6,9 +6,9 @@ from datetime import UTC, date, datetime, timedelta
6
6
 
7
7
  import pytest
8
8
 
9
- from tariffkit import Config, RateEngine, Supplier
9
+ from tariffkit import Config, RateEngine, Supplier, Utility
10
10
  from tariffkit.config import CcaConfig
11
- from tariffkit.errors import ConfigError, OutOfRangeError
11
+ from tariffkit.errors import ConfigError, DataError, OutOfRangeError
12
12
  from tariffkit.timeutil import (
13
13
  PACIFIC,
14
14
  DayType,
@@ -278,6 +278,29 @@ class TestConfigValidation:
278
278
  assert config.cca is not None
279
279
  assert config.cca.name == "MCE"
280
280
 
281
+ def test_canonical_utility_identifier_is_emitted(self) -> None:
282
+ config = Config()
283
+
284
+ assert config.utility is Utility.PACIFIC_GAS_AND_ELECTRIC
285
+ assert config.to_dict()["utility"] == Utility.PACIFIC_GAS_AND_ELECTRIC.value
286
+ assert RateEngine(config).describe()["utility"] == Utility.PACIFIC_GAS_AND_ELECTRIC.value
287
+ assert (
288
+ RateEngine(config).tariff.snapshot_for(pt(2026, 8, 16, 9)).raw["utility"]
289
+ == Utility.PACIFIC_GAS_AND_ELECTRIC.value
290
+ )
291
+
292
+ def test_uppercase_pge_is_not_a_utility_identifier(self) -> None:
293
+ with pytest.raises(ConfigError, match="is not a valid Utility"):
294
+ Config.from_dict({"utility": "PGE"})
295
+
296
+ def test_lowercase_pge_identifies_portland_without_loading_california_rates(self) -> None:
297
+ config = Config.from_dict({"utility": "pge"})
298
+
299
+ assert config.utility is Utility.PORTLAND_GENERAL_ELECTRIC
300
+ assert config.to_dict()["utility"] == "pge"
301
+ with pytest.raises(DataError, match="Portland General Electric pricing is not supported"):
302
+ RateEngine(config)
303
+
281
304
  def test_from_dict_rejects_unknown_keys(self) -> None:
282
305
  with pytest.raises(ConfigError, match="unknown config keys"):
283
306
  Config.from_dict({"supplier": "bundled", "nonsense": 1})
@@ -306,6 +329,7 @@ def test_describe_reports_provenance(engine: RateEngine) -> None:
306
329
  assert info["export_vintage"] == "NBT26"
307
330
  assert info["tariff_effective"] == "2026-03-01"
308
331
  assert info["tariff_advice_letter"] == "7846-E"
332
+ assert info["pto_date"] == "2026-06-03"
309
333
  assert info["lock_end"] == "2035-06-02"
310
334
 
311
335
 
@@ -89,6 +89,10 @@ class TestDiscovery:
89
89
  self, payloads: list[tuple[str, dict[str, Any]]]
90
90
  ) -> None:
91
91
  assert {p["device"]["identifiers"][0] for _, p in payloads} == {"tariffkit"}
92
+ assert {p["device"]["manufacturer"] for _, p in payloads} == {
93
+ "Pacific Gas and Electric Company"
94
+ }
95
+ assert {p["device"]["name"] for _, p in payloads} == {"PG&E Rates"}
92
96
  assert all(p["availability_topic"] == "tariffkit/status" for _, p in payloads)
93
97
 
94
98
  def test_unique_ids_are_distinct(self, payloads: list[tuple[str, dict[str, Any]]]) -> None:
@@ -10,7 +10,7 @@ from datetime import date, datetime
10
10
 
11
11
  import pytest
12
12
 
13
- from tariffkit import Config, RateEngine, Supplier
13
+ from tariffkit import Config, RateEngine, Supplier, Utility
14
14
  from tariffkit.cca import load_rate_card
15
15
  from tariffkit.config import CcaConfig
16
16
  from tariffkit.errors import DataError
@@ -67,6 +67,10 @@ def test_cost_relief_credit_expires_at_the_end_of_2026() -> None:
67
67
  )
68
68
 
69
69
 
70
+ def test_rate_card_uses_canonical_utility_identifier() -> None:
71
+ assert load_rate_card("mce", CARD_DATE).raw["utility"] == "pacific_gas_and_electric"
72
+
73
+
70
74
  @pytest.mark.parametrize("schedule", ["E-ELEC", "E-TOU-C", "EV2-A"])
71
75
  def test_mce_generation_is_at_parity_with_pge_today(schedule: str) -> None:
72
76
  """Parity is real but not guaranteed -- fail loudly if MCE diverges.
@@ -75,7 +79,7 @@ def test_mce_generation_is_at_parity_with_pge_today(schedule: str) -> None:
75
79
  a bug in the engine. Checked per schedule, since MCE prices each separately.
76
80
  """
77
81
  card = load_rate_card("mce", CARD_DATE)
78
- snapshot = load_snapshot("PGE", schedule, date(2026, 6, 1))
82
+ snapshot = load_snapshot(Utility.PACIFIC_GAS_AND_ELECTRIC, schedule, date(2026, 6, 1))
79
83
  for season, periods in snapshot.raw["energy"].items():
80
84
  for period, components in periods.items():
81
85
  assert card.generation(schedule, season, period) == pytest.approx(
@@ -11,7 +11,7 @@ from datetime import date, datetime, timedelta
11
11
 
12
12
  import pytest
13
13
 
14
- from tariffkit import Config, RateEngine, Season, TouPeriod
14
+ from tariffkit import Config, RateEngine, Season, TouPeriod, Utility
15
15
  from tariffkit.billing.engine import BillEngine
16
16
  from tariffkit.billing.models import Bill, BillingPeriod, IntervalReading
17
17
  from tariffkit.errors import ConfigError
@@ -26,7 +26,7 @@ def cells() -> list[tuple[str, str, str]]:
26
26
  """(tariff, season, period) for every rate the sheets publish."""
27
27
  found = []
28
28
  for tariff in SCHEDULES:
29
- totals = load_snapshot("PGE", tariff, EFFECTIVE).raw["totals"]
29
+ totals = load_snapshot(Utility.PACIFIC_GAS_AND_ELECTRIC, tariff, EFFECTIVE).raw["totals"]
30
30
  for season, periods in totals.items():
31
31
  found.extend((tariff, season, period) for period in periods)
32
32
  return found
@@ -40,7 +40,7 @@ def test_components_sum_to_published_total(tariff: str, season: str, period: str
40
40
  On E-TOU-C the total is the over-baseline one, because [adders] carries the
41
41
  over-baseline Conservation Incentive Adjustment.
42
42
  """
43
- raw = load_snapshot("PGE", tariff, EFFECTIVE).raw
43
+ raw = load_snapshot(Utility.PACIFIC_GAS_AND_ELECTRIC, tariff, EFFECTIVE).raw
44
44
  total = sum(raw["energy"][season][period].values()) + sum(raw["adders"].values())
45
45
  assert total == pytest.approx(raw["totals"][season][period], abs=5e-6)
46
46
 
@@ -49,7 +49,7 @@ def test_components_sum_to_published_total(tariff: str, season: str, period: str
49
49
  def test_every_published_cell_is_reachable_by_pricing(tariff: str) -> None:
50
50
  """Guards against a rate table nothing can select, e.g. a stray period."""
51
51
  engine = RateEngine(Config(tariff=tariff))
52
- totals = load_snapshot("PGE", tariff, EFFECTIVE).raw["totals"]
52
+ totals = load_snapshot(Utility.PACIFIC_GAS_AND_ELECTRIC, tariff, EFFECTIVE).raw["totals"]
53
53
  priced = {
54
54
  (str(p.import_price.season), str(p.import_price.period))
55
55
  for month in (7, 12)
@@ -111,9 +111,14 @@ class TestSharedRiders:
111
111
 
112
112
  @pytest.mark.parametrize("table", ["pcia_vintages", "franchise_fee_vintages"])
113
113
  def test_all_schedules_agree(self, table: str) -> None:
114
- reference = load_snapshot("PGE", "E-ELEC", EFFECTIVE).raw["cca"][table]
114
+ reference = load_snapshot(Utility.PACIFIC_GAS_AND_ELECTRIC, "E-ELEC", EFFECTIVE).raw["cca"][
115
+ table
116
+ ]
115
117
  for tariff in SCHEDULES:
116
- assert load_snapshot("PGE", tariff, EFFECTIVE).raw["cca"][table] == reference, tariff
118
+ assert (
119
+ load_snapshot(Utility.PACIFIC_GAS_AND_ELECTRIC, tariff, EFFECTIVE).raw["cca"][table]
120
+ == reference
121
+ ), tariff
117
122
 
118
123
  @pytest.mark.parametrize("tariff", SCHEDULES)
119
124
  def test_base_services_charge_is_the_same_ab205_charge(self, tariff: str) -> None:
@@ -149,7 +154,7 @@ class TestBaseline:
149
154
 
150
155
  def test_credit_is_the_spread_between_the_two_cia_rates(self) -> None:
151
156
  """The bill prints one credit; the sheet implements it as two rates."""
152
- raw = load_snapshot("PGE", "E-TOU-C", EFFECTIVE).raw["baseline"]
157
+ raw = load_snapshot(Utility.PACIFIC_GAS_AND_ELECTRIC, "E-TOU-C", EFFECTIVE).raw["baseline"]
153
158
  assert raw["over_rate"] - raw["within_rate"] == pytest.approx(raw["credit"])
154
159
  price = RateEngine(Config(tariff="E-TOU-C")).price_at(self.midday(12)).import_price
155
160
  assert price.baseline_credit == pytest.approx(0.08140)
File without changes
File without changes
File without changes
File without changes