tariffkit 0.2.0__py3-none-any.whl

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 (83) hide show
  1. tariffkit/__init__.py +52 -0
  2. tariffkit/account/__init__.py +41 -0
  3. tariffkit/account/cli.py +493 -0
  4. tariffkit/account/errors.py +25 -0
  5. tariffkit/account/model.py +641 -0
  6. tariffkit/account/rates.py +61 -0
  7. tariffkit/account/repository.py +329 -0
  8. tariffkit/billing/__init__.py +56 -0
  9. tariffkit/billing/engine.py +505 -0
  10. tariffkit/billing/ledger.py +365 -0
  11. tariffkit/billing/models.py +274 -0
  12. tariffkit/billing/netting.py +137 -0
  13. tariffkit/billing/trueup.py +498 -0
  14. tariffkit/cca.py +122 -0
  15. tariffkit/cli.py +809 -0
  16. tariffkit/config.py +296 -0
  17. tariffkit/data/__init__.py +40 -0
  18. tariffkit/data/cca/mce/2023-01-01.toml +61 -0
  19. tariffkit/data/cca/mce/2026-04-01.toml +152 -0
  20. tariffkit/data/export/pge/acc_plus/2023-04-15.toml +43 -0
  21. tariffkit/data/export/pge/nbt00.json.gz +0 -0
  22. tariffkit/data/export/pge/nbt23.json.gz +0 -0
  23. tariffkit/data/export/pge/nbt24.json.gz +0 -0
  24. tariffkit/data/export/pge/nbt25.json.gz +0 -0
  25. tariffkit/data/export/pge/nbt26.json.gz +0 -0
  26. tariffkit/data/holidays.toml +36 -0
  27. tariffkit/data/manifest.json +56 -0
  28. tariffkit/data/nsc/pge.toml +57 -0
  29. tariffkit/data/tariff/pge/eelec/2025-01-01.toml +151 -0
  30. tariffkit/data/tariff/pge/eelec/2025-03-01.toml +150 -0
  31. tariffkit/data/tariff/pge/eelec/2025-09-01.toml +150 -0
  32. tariffkit/data/tariff/pge/eelec/2026-01-01.toml +153 -0
  33. tariffkit/data/tariff/pge/eelec/2026-03-01.toml +157 -0
  34. tariffkit/data/tariff/pge/etouc/2025-01-01.toml +222 -0
  35. tariffkit/data/tariff/pge/etouc/2025-03-01.toml +221 -0
  36. tariffkit/data/tariff/pge/etouc/2025-09-01.toml +221 -0
  37. tariffkit/data/tariff/pge/etouc/2026-01-01.toml +224 -0
  38. tariffkit/data/tariff/pge/etouc/2026-03-01.toml +231 -0
  39. tariffkit/data/tariff/pge/ev2a/2025-01-01.toml +144 -0
  40. tariffkit/data/tariff/pge/ev2a/2025-03-01.toml +143 -0
  41. tariffkit/data/tariff/pge/ev2a/2025-09-01.toml +143 -0
  42. tariffkit/data/tariff/pge/ev2a/2026-01-01.toml +146 -0
  43. tariffkit/data/tariff/pge/ev2a/2026-03-01.toml +153 -0
  44. tariffkit/data/tax/ca_energy_resources/2025-01-01.toml +27 -0
  45. tariffkit/data/tax/ca_energy_resources/2026-01-01.toml +27 -0
  46. tariffkit/data/versioned.py +118 -0
  47. tariffkit/engine.py +82 -0
  48. tariffkit/errors.py +19 -0
  49. tariffkit/export/__init__.py +5 -0
  50. tariffkit/export/nbt.py +207 -0
  51. tariffkit/interop/__init__.py +21 -0
  52. tariffkit/interop/emhass.py +79 -0
  53. tariffkit/interop/predbat.py +102 -0
  54. tariffkit/interop/slots.py +63 -0
  55. tariffkit/models.py +164 -0
  56. tariffkit/mqtt/__init__.py +6 -0
  57. tariffkit/mqtt/discovery.py +84 -0
  58. tariffkit/mqtt/publisher.py +305 -0
  59. tariffkit/providers/__init__.py +1 -0
  60. tariffkit/providers/pge/__init__.py +33 -0
  61. tariffkit/providers/pge/reconcile.py +828 -0
  62. tariffkit/providers/pge/statements/__init__.py +26 -0
  63. tariffkit/providers/pge/statements/errors.py +20 -0
  64. tariffkit/providers/pge/statements/model.py +320 -0
  65. tariffkit/providers/pge/statements/ocr.py +193 -0
  66. tariffkit/providers/pge/statements/parse.py +813 -0
  67. tariffkit/py.typed +0 -0
  68. tariffkit/secrets.py +164 -0
  69. tariffkit/sources/__init__.py +71 -0
  70. tariffkit/sources/greenbutton.py +318 -0
  71. tariffkit/sources/homeassistant.py +342 -0
  72. tariffkit/sources/influx.py +359 -0
  73. tariffkit/sources/pge.py +1153 -0
  74. tariffkit/tariff/__init__.py +5 -0
  75. tariffkit/tariff/retail.py +271 -0
  76. tariffkit/timeutil.py +120 -0
  77. tariffkit/web/__init__.py +5 -0
  78. tariffkit/web/app.py +220 -0
  79. tariffkit-0.2.0.dist-info/METADATA +260 -0
  80. tariffkit-0.2.0.dist-info/RECORD +83 -0
  81. tariffkit-0.2.0.dist-info/WHEEL +4 -0
  82. tariffkit-0.2.0.dist-info/entry_points.txt +2 -0
  83. tariffkit-0.2.0.dist-info/licenses/LICENSE +21 -0
tariffkit/cca.py ADDED
@@ -0,0 +1,122 @@
1
+ """Community Choice Aggregation generation rate cards.
2
+
3
+ A CCA supplies generation while PG&E still delivers, so a CCA customer's import
4
+ price is PG&E's delivery stack plus the CCA's own generation rates, a vintaged
5
+ PCIA, and a franchise fee surcharge. Only providers with a vendored card here
6
+ can be priced end to end; everything else needs rates supplied via CcaConfig.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from datetime import date
13
+ from functools import lru_cache
14
+ from typing import Any
15
+
16
+ from .data import versioned
17
+ from .errors import ConfigError, DataError
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class CcaRateCard:
22
+ """A vendored CCA generation rate card."""
23
+
24
+ provider: str
25
+ raw: dict[str, Any]
26
+
27
+ @property
28
+ def effective(self) -> date:
29
+ return date.fromisoformat(self.raw["effective"])
30
+
31
+ @property
32
+ def source_url(self) -> str:
33
+ return str(self.raw.get("source_url", ""))
34
+
35
+ def generation(
36
+ self, schedule: str, season: str, period: str, option: str = "light_green"
37
+ ) -> float:
38
+ """Generation charge in $/kWh, including any product premium.
39
+
40
+ Keyed by PG&E schedule: a CCA prices each one separately, and the rates
41
+ differ enough that borrowing another schedule's card is a large silent
42
+ error rather than an approximation. MCE winter off-peak is 0.06754 on
43
+ E-ELEC against 0.11042 on E-TOU-C.
44
+ """
45
+ slug = schedule.lower().replace("-", "")
46
+ by_schedule = self.raw["generation"]
47
+ if slug not in by_schedule:
48
+ raise DataError(
49
+ f"{self.provider}: no generation rates for schedule {schedule!r}; "
50
+ f"this card covers {sorted(by_schedule)}"
51
+ )
52
+ try:
53
+ base = float(by_schedule[slug][season][period])
54
+ except KeyError as exc:
55
+ raise DataError(
56
+ f"{self.provider}: no {schedule} generation rate for {season}/{period}"
57
+ ) from exc
58
+ premium = self.raw.get("options", {}).get(option)
59
+ if premium is None:
60
+ raise ConfigError(
61
+ f"{self.provider}: unknown product option {option!r}; "
62
+ f"available: {sorted(self.raw.get('options', {}))}"
63
+ )
64
+ return base + float(premium)
65
+
66
+ def cost_relief_credit(self, on: date) -> float:
67
+ """A time-limited per-kWh credit, or zero once it lapses.
68
+
69
+ Dated rather than folded into the rates so it stops applying on its own
70
+ instead of quietly understating prices in later years.
71
+ """
72
+ credit = self.raw.get("cost_relief_credit")
73
+ if not credit:
74
+ return 0.0
75
+ through = credit.get("through")
76
+ if through and on > date.fromisoformat(through):
77
+ return 0.0
78
+ return float(credit["rate"])
79
+
80
+ @property
81
+ def solar_bonus_fraction(self) -> float:
82
+ """Bonus paid as a fraction of the base export credit."""
83
+ return float(self.raw.get("export", {}).get("solar_bonus_fraction", 0.0))
84
+
85
+ @property
86
+ def export_credit_verified(self) -> bool:
87
+ """Whether this provider's export credit basis has been confirmed."""
88
+ return bool(self.raw.get("export", {}).get("export_credit_verified", False))
89
+
90
+
91
+ @lru_cache(maxsize=8)
92
+ def load_rate_card(provider: str, on: date) -> CcaRateCard:
93
+ """The provider's rate card in force on ``on``.
94
+
95
+ A CCA reprices on a date like any other supplier -- MCE cut generation by
96
+ about 14% effective 2026-04-01 -- so a card is chosen by date rather than
97
+ "whichever is vendored". Before this took a date at all, a January bill was
98
+ priced with April's card and looked entirely reasonable while being wrong by
99
+ the whole repricing.
100
+ """
101
+ slug = provider.lower().replace(" ", "_")
102
+ relative = f"cca/{slug}"
103
+ try:
104
+ version = versioned.load(relative, on, label=f"CCA {provider}")
105
+ except DataError as exc:
106
+ # Distinguish "we vendor nothing for this CCA" -- where the answer is to
107
+ # supply rates yourself -- from "we vendor it but not back that far",
108
+ # where the answer is to vendor the missing vintage. versioned.load
109
+ # phrases the second; only the first needs rewriting here.
110
+ if "no vendored data at" not in str(exc):
111
+ raise
112
+ raise DataError(
113
+ f"no vendored rate card for CCA {provider!r}; supply rates via "
114
+ f"CcaConfig(generation_rates=..., franchise_fee_surcharge=...)"
115
+ ) from exc
116
+ if version.raw.get("schema") != 1:
117
+ raise DataError(f"{provider}: unsupported rate card schema {version.raw.get('schema')}")
118
+ return CcaRateCard(provider=str(version.raw["provider"]), raw=version.raw)
119
+
120
+
121
+ def available_rate_cards() -> tuple[str, ...]:
122
+ return ("MCE",)