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.
- tariffkit/__init__.py +52 -0
- tariffkit/account/__init__.py +41 -0
- tariffkit/account/cli.py +493 -0
- tariffkit/account/errors.py +25 -0
- tariffkit/account/model.py +641 -0
- tariffkit/account/rates.py +61 -0
- tariffkit/account/repository.py +329 -0
- tariffkit/billing/__init__.py +56 -0
- tariffkit/billing/engine.py +505 -0
- tariffkit/billing/ledger.py +365 -0
- tariffkit/billing/models.py +274 -0
- tariffkit/billing/netting.py +137 -0
- tariffkit/billing/trueup.py +498 -0
- tariffkit/cca.py +122 -0
- tariffkit/cli.py +809 -0
- tariffkit/config.py +296 -0
- tariffkit/data/__init__.py +40 -0
- tariffkit/data/cca/mce/2023-01-01.toml +61 -0
- tariffkit/data/cca/mce/2026-04-01.toml +152 -0
- tariffkit/data/export/pge/acc_plus/2023-04-15.toml +43 -0
- tariffkit/data/export/pge/nbt00.json.gz +0 -0
- tariffkit/data/export/pge/nbt23.json.gz +0 -0
- tariffkit/data/export/pge/nbt24.json.gz +0 -0
- tariffkit/data/export/pge/nbt25.json.gz +0 -0
- tariffkit/data/export/pge/nbt26.json.gz +0 -0
- tariffkit/data/holidays.toml +36 -0
- tariffkit/data/manifest.json +56 -0
- tariffkit/data/nsc/pge.toml +57 -0
- tariffkit/data/tariff/pge/eelec/2025-01-01.toml +151 -0
- tariffkit/data/tariff/pge/eelec/2025-03-01.toml +150 -0
- tariffkit/data/tariff/pge/eelec/2025-09-01.toml +150 -0
- tariffkit/data/tariff/pge/eelec/2026-01-01.toml +153 -0
- tariffkit/data/tariff/pge/eelec/2026-03-01.toml +157 -0
- tariffkit/data/tariff/pge/etouc/2025-01-01.toml +222 -0
- tariffkit/data/tariff/pge/etouc/2025-03-01.toml +221 -0
- tariffkit/data/tariff/pge/etouc/2025-09-01.toml +221 -0
- tariffkit/data/tariff/pge/etouc/2026-01-01.toml +224 -0
- tariffkit/data/tariff/pge/etouc/2026-03-01.toml +231 -0
- tariffkit/data/tariff/pge/ev2a/2025-01-01.toml +144 -0
- tariffkit/data/tariff/pge/ev2a/2025-03-01.toml +143 -0
- tariffkit/data/tariff/pge/ev2a/2025-09-01.toml +143 -0
- tariffkit/data/tariff/pge/ev2a/2026-01-01.toml +146 -0
- tariffkit/data/tariff/pge/ev2a/2026-03-01.toml +153 -0
- tariffkit/data/tax/ca_energy_resources/2025-01-01.toml +27 -0
- tariffkit/data/tax/ca_energy_resources/2026-01-01.toml +27 -0
- tariffkit/data/versioned.py +118 -0
- tariffkit/engine.py +82 -0
- tariffkit/errors.py +19 -0
- tariffkit/export/__init__.py +5 -0
- tariffkit/export/nbt.py +207 -0
- tariffkit/interop/__init__.py +21 -0
- tariffkit/interop/emhass.py +79 -0
- tariffkit/interop/predbat.py +102 -0
- tariffkit/interop/slots.py +63 -0
- tariffkit/models.py +164 -0
- tariffkit/mqtt/__init__.py +6 -0
- tariffkit/mqtt/discovery.py +84 -0
- tariffkit/mqtt/publisher.py +305 -0
- tariffkit/providers/__init__.py +1 -0
- tariffkit/providers/pge/__init__.py +33 -0
- tariffkit/providers/pge/reconcile.py +828 -0
- tariffkit/providers/pge/statements/__init__.py +26 -0
- tariffkit/providers/pge/statements/errors.py +20 -0
- tariffkit/providers/pge/statements/model.py +320 -0
- tariffkit/providers/pge/statements/ocr.py +193 -0
- tariffkit/providers/pge/statements/parse.py +813 -0
- tariffkit/py.typed +0 -0
- tariffkit/secrets.py +164 -0
- tariffkit/sources/__init__.py +71 -0
- tariffkit/sources/greenbutton.py +318 -0
- tariffkit/sources/homeassistant.py +342 -0
- tariffkit/sources/influx.py +359 -0
- tariffkit/sources/pge.py +1153 -0
- tariffkit/tariff/__init__.py +5 -0
- tariffkit/tariff/retail.py +271 -0
- tariffkit/timeutil.py +120 -0
- tariffkit/web/__init__.py +5 -0
- tariffkit/web/app.py +220 -0
- tariffkit-0.2.0.dist-info/METADATA +260 -0
- tariffkit-0.2.0.dist-info/RECORD +83 -0
- tariffkit-0.2.0.dist-info/WHEEL +4 -0
- tariffkit-0.2.0.dist-info/entry_points.txt +2 -0
- 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",)
|