Skip to content

Commit 379b331

Browse files
committed
Expose Rider 18 export status
1 parent 53a79f0 commit 379b331

8 files changed

Lines changed: 105 additions & 4 deletions

File tree

custom_components/dte_rates/config_flow.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from .const import CONF_NET_METERING, CONF_SELECTED_RATE, DOMAIN
88
from .coordinator import DteRateCoordinator
9+
from .models import ParsedRateCard
910

1011

1112
class DteRatesConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@@ -35,4 +36,22 @@ async def async_step_user(self, user_input: dict | None = None) -> FlowResult:
3536
vol.Optional(CONF_NET_METERING, default=False): bool,
3637
}
3738
)
38-
return self.async_show_form(step_id="user", data_schema=schema)
39+
return self.async_show_form(
40+
step_id="user",
41+
data_schema=schema,
42+
description_placeholders={
43+
"rider18_status": _rider18_status(coordinator.data),
44+
},
45+
)
46+
47+
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."

custom_components/dte_rates/const.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
ATTR_MONTHLY_COMPONENTS = "monthly_components"
2525
ATTR_SOURCE_URL = "source_url"
2626
ATTR_RIDER18_SOURCE_URL = "rider18_source_url"
27+
ATTR_RIDER18_EXPORT_AVAILABLE = "rider18_export_available"
28+
ATTR_EXPORT_RATE_SOURCE = "export_rate_source"
29+
ATTR_EXPORT_RATE_WARNING = "export_rate_warning"
2730
ATTR_CARD_EFFECTIVE_DATE = "card_effective_date"
2831
ATTR_SELECTED_RATE_AVAILABLE = "selected_rate_available"
2932
ATTR_WARNING = "warning"

custom_components/dte_rates/sensor.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@
2020
ATTR_MONTHLY_COMPONENTS,
2121
ATTR_PERIOD,
2222
ATTR_CURRENT_RATE_NAME,
23+
ATTR_EXPORT_RATE_SOURCE,
24+
ATTR_EXPORT_RATE_WARNING,
2325
ATTR_NEXT_RATE_CHANGE,
2426
ATTR_NEXT_RATE_NAME,
2527
ATTR_NEXT_RATE_VALUE,
2628
ATTR_RATE_CODE,
2729
ATTR_RATE_NAME,
30+
ATTR_RIDER18_EXPORT_AVAILABLE,
2831
ATTR_RIDER18_SOURCE_URL,
2932
ATTR_SCHEDULE_BY_SEASON,
3033
ATTR_SCHEDULE_TEXT,
@@ -123,6 +126,25 @@ def _export_rate_cents(self, period: SeasonalPeriodRate):
123126
self._rider18_export_rate_cents(period),
124127
)
125128

129+
def _export_rate_source(self, period: SeasonalPeriodRate) -> str:
130+
if self._entry.data.get(CONF_NET_METERING, False):
131+
return "net_metering"
132+
if self._rider18_export_rate_cents(period) is not None:
133+
return "rider18"
134+
return "pdf_generation_components"
135+
136+
def _export_rate_warning(self, period: SeasonalPeriodRate) -> str | None:
137+
if self._entry.data.get(CONF_NET_METERING, False):
138+
return None
139+
if self._rider18_export_rate_cents(period) is not None:
140+
return None
141+
if not self.coordinator.data.rider18_source_url:
142+
return "Rider 18 export credits are not loaded; using PDF generation-only export pricing."
143+
return (
144+
"No Rider 18 export credit matched the selected rate's active season and period; "
145+
"using PDF generation-only export pricing."
146+
)
147+
126148
def _warning(self) -> str | None:
127149
selected = self._entry.data[CONF_SELECTED_RATE]
128150
if selected not in self.coordinator.data.rates:
@@ -282,6 +304,13 @@ def native_value(self) -> float | None:
282304
def extra_state_attributes(self) -> dict:
283305
attrs = self._base_attributes()
284306
attrs[CONF_NET_METERING] = self._entry.data.get(CONF_NET_METERING, False)
307+
period = self._active_period()
308+
if period is not None:
309+
attrs[ATTR_EXPORT_RATE_SOURCE] = self._export_rate_source(period)
310+
attrs[ATTR_RIDER18_EXPORT_AVAILABLE] = self._rider18_export_rate_cents(period) is not None
311+
warning = self._export_rate_warning(period)
312+
if warning is not None:
313+
attrs[ATTR_EXPORT_RATE_WARNING] = warning
285314
return attrs
286315

287316
def _period_value_usd(self, period: SeasonalPeriodRate | None) -> float | None:

custom_components/dte_rates/strings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"step": {
44
"user": {
55
"title": "DTE Residential Rates",
6-
"description": "Choose your DTE residential electric pricing plan.",
6+
"description": "Choose your DTE residential electric pricing plan.\n\n{rider18_status}",
77
"data": {
88
"selected_rate": "Rate plan",
99
"net_metering": "Net metering enabled"

custom_components/dte_rates/translations/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"step": {
44
"user": {
55
"title": "DTE Residential Rates",
6-
"description": "Choose your DTE residential electric pricing plan.",
6+
"description": "Choose your DTE residential electric pricing plan.\n\n{rider18_status}",
77
"data": {
88
"selected_rate": "Rate plan",
99
"net_metering": "Net metering enabled"

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def __init__(self):
3232
self.hass = None
3333

3434
def async_show_form(self, *, step_id, data_schema=None, errors=None, **kwargs):
35-
return FlowResult(type="form", step_id=step_id, data_schema=data_schema, errors=errors or {})
35+
return FlowResult(type="form", step_id=step_id, data_schema=data_schema, errors=errors or {}, **kwargs)
3636

3737
def async_create_entry(self, *, title, data):
3838
return FlowResult(type="create_entry", title=title, data=data)

tests/test_config_flow.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from decimal import Decimal
34
from unittest.mock import MagicMock
45

56
import pytest
@@ -41,3 +42,34 @@ async def _refresh(self):
4142
assert result["title"] == "Overnight (D1.13)"
4243
assert result["data"][CONF_SELECTED_RATE] == "D1.13"
4344
assert result["data"][CONF_NET_METERING] is True
45+
46+
47+
@pytest.mark.asyncio
48+
async def test_config_flow_describes_rider18_loaded_status(monkeypatch):
49+
flow = DteRatesConfigFlow()
50+
flow.hass = MagicMock()
51+
52+
async def _refresh(self):
53+
self.data = ParsedRateCard(
54+
source_url="https://example.test/card.pdf",
55+
effective_date="February 6, 2025",
56+
rates={
57+
"D1.11": RatePlan(code="D1.11", name="Standard Base", periods=[]),
58+
"D1.13": RatePlan(code="D1.13", name="Overnight", periods=[]),
59+
},
60+
raw_text_hash="abc",
61+
rider18_source_url="https://example.test/Rider18Calculator.xlsx",
62+
rider18_export_rates={
63+
"D1.11": {("october_through_may", "off_peak"): Decimal("10.586")},
64+
},
65+
)
66+
67+
monkeypatch.setattr(
68+
"custom_components.dte_rates.coordinator.DteRateCoordinator.async_refresh",
69+
_refresh,
70+
)
71+
72+
result = await flow.async_step_user()
73+
74+
assert result["type"] == "form"
75+
assert result["description_placeholders"]["rider18_status"] == "Rider 18 export credits loaded for 1 rate plan."

tests/test_sensor.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ def test_export_sensor_prefers_rider18_credit_without_net_metering(monkeypatch):
8484
sensor = DteExportRateSensor(coordinator, entry)
8585
assert sensor.native_value == 0.10586
8686
assert sensor.extra_state_attributes["rider18_source_url"] == "https://example.test/Rider18Calculator.xlsx"
87+
assert sensor.extra_state_attributes["export_rate_source"] == "rider18"
88+
assert sensor.extra_state_attributes["rider18_export_available"] is True
8789

8890

8991
def test_export_sensor_ignores_rider18_credit_with_net_metering(monkeypatch):
@@ -94,6 +96,22 @@ def test_export_sensor_ignores_rider18_credit_with_net_metering(monkeypatch):
9496

9597
sensor = DteExportRateSensor(coordinator, entry)
9698
assert sensor.native_value == 0.06
99+
assert sensor.extra_state_attributes["export_rate_source"] == "net_metering"
100+
101+
102+
def test_export_sensor_reports_generation_fallback_when_rider18_missing(monkeypatch):
103+
monkeypatch.setattr("custom_components.dte_rates.sensor.dt_util.now", lambda: datetime(2026, 3, 1, 12, 0))
104+
105+
coordinator = _coordinator_with_rate({("june_through_september", "peak"): Decimal("16.284")})
106+
entry = SimpleNamespace(entry_id="entry_16", data={CONF_SELECTED_RATE: "D1.11", CONF_NET_METERING: False})
107+
108+
sensor = DteExportRateSensor(coordinator, entry)
109+
attrs = sensor.extra_state_attributes
110+
111+
assert sensor.native_value == 0.03
112+
assert attrs["export_rate_source"] == "pdf_generation_components"
113+
assert attrs["rider18_export_available"] is False
114+
assert "No Rider 18 export credit" in attrs["export_rate_warning"]
97115

98116

99117
def test_sensor_warns_when_selected_rate_disappears(monkeypatch):

0 commit comments

Comments
 (0)