Skip to content

Commit 2713b07

Browse files
committed
Calculate Rider 18 export from rate card
1 parent 2575c4f commit 2713b07

13 files changed

Lines changed: 97 additions & 415 deletions

custom_components/dte_rates/__init__.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,9 @@ async def _handle_show_schedule_service(call) -> None:
122122

123123
lines_by_season: dict[str, list[str]] = defaultdict(list)
124124
net_metering = entry.data.get(CONF_NET_METERING, False) if entry else False
125-
rider18_rates = getattr(coordinator.data, "rider18_export_rates", {})
126125
for period in sorted(rate.periods, key=lambda p: (p.season_name, p.period_name)):
127126
import_usd = float(current_import_rate_cents(period) / 100)
128-
rider18_export_cents = None
129-
if not net_metering:
130-
rider18_export_cents = rider18_rates.get(rate.code, {}).get((period.season_name, period.period_name))
131-
export_usd = float(current_export_rate_cents(period, net_metering, rider18_export_cents) / 100)
127+
export_usd = float(current_export_rate_cents(period, net_metering) / 100)
132128
lines_by_season[period.season_name].append(
133129
f"{period_display_name(period)}: Import ${import_usd:.4f}/kWh | Export ${export_usd:.4f}/kWh"
134130
)

custom_components/dte_rates/config_flow.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
from .const import CONF_NET_METERING, CONF_SELECTED_RATE, DOMAIN
88
from .coordinator import DteRateCoordinator
9-
from .models import ParsedRateCard
109

1110

1211
class DteRatesConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@@ -45,13 +44,5 @@ async def async_step_user(self, user_input: dict | None = None) -> FlowResult:
4544
)
4645

4746

48-
def _rider18_status(rate_card: ParsedRateCard) -> str:
49-
count = len(rate_card.rider18_export_rates)
50-
if count:
51-
suffix = "rate plan" if count == 1 else "rate plans"
52-
return f"Rider 18 export credits loaded for {count} {suffix}."
53-
54-
if rate_card.rider18_source_url:
55-
return "Rider 18 calculator loaded, but no export credits matched the parsed rate plans."
56-
57-
return "Rider 18 export credits are not loaded; non-net-metering exports will fall back to PDF generation-only pricing."
47+
def _rider18_status(_rate_card) -> str:
48+
return "Rider 18 export credits are calculated from the parsed rate card as Generation + Distribution/Transmission."

custom_components/dte_rates/const.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@
66
"residential/Service-Request/pricing/residential-pricing-options/"
77
"ResidentialElectricRateCard.pdf"
88
)
9-
RIDER18_CALCULATOR_URL = (
10-
"https://www.dteenergy.com/content/dam/dteenergy/deg/website/"
11-
"hybris/rooftop-solar/Rider18Calculator.xlsx"
12-
)
139

1410
UPDATE_INTERVAL = timedelta(days=7)
1511

@@ -23,7 +19,6 @@
2319
ATTR_COMPONENTS = "components"
2420
ATTR_MONTHLY_COMPONENTS = "monthly_components"
2521
ATTR_SOURCE_URL = "source_url"
26-
ATTR_RIDER18_SOURCE_URL = "rider18_source_url"
2722
ATTR_RIDER18_EXPORT_AVAILABLE = "rider18_export_available"
2823
ATTR_EXPORT_RATE_SOURCE = "export_rate_source"
2924
ATTR_EXPORT_RATE_WARNING = "export_rate_warning"

custom_components/dte_rates/coordinator.py

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@
77
from homeassistant.helpers.aiohttp_client import async_get_clientsession
88
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
99

10-
from .const import RATE_CARD_URL, RIDER18_CALCULATOR_URL, UPDATE_INTERVAL
10+
from .const import RATE_CARD_URL, UPDATE_INTERVAL
1111
from .models import ParsedRateCard
1212
from .pdf_parser import parse_rate_card_pdf
13-
from .rider18_parser import parse_rider18_xlsx
1413

1514
_LOGGER = logging.getLogger(__name__)
1615

@@ -34,39 +33,11 @@ async def _async_update_data(self) -> ParsedRateCard:
3433
except Exception as err:
3534
raise UpdateFailed(f"Failed downloading DTE rate card: {err}") from err
3635

37-
rider18_bytes: bytes | None = None
3836
try:
39-
async with session.get(RIDER18_CALCULATOR_URL, timeout=60) as resp:
40-
resp.raise_for_status()
41-
rider18_bytes = await resp.read()
42-
except Exception as err:
43-
_LOGGER.warning(
44-
"Failed downloading DTE Rider 18 calculator; export rates will fall back to generation-only values: %s",
45-
err,
46-
)
47-
48-
try:
49-
parsed = await self.hass.async_add_executor_job(
37+
return await self.hass.async_add_executor_job(
5038
parse_rate_card_pdf,
5139
pdf_bytes,
5240
RATE_CARD_URL,
5341
)
5442
except Exception as err:
5543
raise UpdateFailed(f"Failed parsing DTE rate card: {err}") from err
56-
57-
if rider18_bytes is None:
58-
return parsed
59-
60-
try:
61-
parsed.rider18_export_rates = await self.hass.async_add_executor_job(
62-
parse_rider18_xlsx,
63-
rider18_bytes,
64-
)
65-
parsed.rider18_source_url = RIDER18_CALCULATOR_URL
66-
except Exception as err:
67-
_LOGGER.warning(
68-
"Failed parsing DTE Rider 18 calculator; export rates will fall back to generation-only values: %s",
69-
err,
70-
)
71-
72-
return parsed

custom_components/dte_rates/models.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,3 @@ class ParsedRateCard:
5050
effective_date: str | None
5151
rates: dict[str, RatePlan]
5252
raw_text_hash: str
53-
rider18_source_url: str | None = None
54-
rider18_export_rates: dict[str, dict[tuple[str, str], Decimal]] = field(default_factory=dict)

custom_components/dte_rates/rate_calculator.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88

99
GENERATION_COMPONENT_MARKERS = ("capacity_energy", "non_capacity_energy")
10+
DISTRIBUTION_TRANSMISSION_COMPONENT_MARKERS = ("distribution", "transmission")
1011

1112

1213
def _is_hour_in_range(start_hour: int, end_hour: int, hour: int) -> bool:
@@ -65,22 +66,37 @@ def current_import_rate_cents(period: SeasonalPeriodRate) -> Decimal:
6566
return period.components.per_kwh_total
6667

6768

68-
def current_export_rate_cents(
69-
period: SeasonalPeriodRate,
70-
net_metering: bool,
71-
rider18_export_cents: Decimal | None = None,
72-
) -> Decimal:
69+
def _component_total(period: SeasonalPeriodRate, markers: tuple[str, ...]) -> Decimal:
70+
total = Decimal("0")
71+
for key, value in period.components.per_kwh.items():
72+
if any(marker in key for marker in markers):
73+
total += value
74+
return total
75+
76+
77+
def _has_component(period: SeasonalPeriodRate, markers: tuple[str, ...]) -> bool:
78+
return any(any(marker in key for marker in markers) for key in period.components.per_kwh)
79+
80+
81+
def rider18_export_rate_cents(period: SeasonalPeriodRate) -> Decimal:
82+
return _component_total(period, GENERATION_COMPONENT_MARKERS) + _component_total(
83+
period,
84+
DISTRIBUTION_TRANSMISSION_COMPONENT_MARKERS,
85+
)
86+
87+
88+
def rider18_export_formula_available(period: SeasonalPeriodRate) -> bool:
89+
return _has_component(period, GENERATION_COMPONENT_MARKERS) and _has_component(
90+
period,
91+
DISTRIBUTION_TRANSMISSION_COMPONENT_MARKERS,
92+
)
93+
94+
95+
def current_export_rate_cents(period: SeasonalPeriodRate, net_metering: bool) -> Decimal:
7396
if net_metering:
7497
return period.components.per_kwh_total
7598

76-
if rider18_export_cents is not None:
77-
return rider18_export_cents
78-
79-
generation_only = Decimal("0")
80-
for key, value in period.components.per_kwh.items():
81-
if any(marker in key for marker in GENERATION_COMPONENT_MARKERS):
82-
generation_only += value
83-
return generation_only
99+
return rider18_export_rate_cents(period)
84100

85101

86102
def period_display_name(period: SeasonalPeriodRate) -> str:

custom_components/dte_rates/rider18_parser.py

Lines changed: 0 additions & 175 deletions
This file was deleted.

0 commit comments

Comments
 (0)