Skip to content

Commit fb15f12

Browse files
committed
Expose current rate calculation details
1 parent c3e7dde commit fb15f12

7 files changed

Lines changed: 114 additions & 0 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ During setup you choose:
5656
- **Net metering enabled** (checkbox).
5757
- **Include PSCR** (enabled by default).
5858
- **Tax rate (%)** (free-form, defaults to `4.0`; set to `0` to omit tax).
59+
- Detroit residents may incur an additional 5.0% City Utility Users' Tax; adjust the free-form tax rate if that applies to your service address.
5960

6061
## Entities Created
6162

@@ -84,6 +85,8 @@ Core rate entities include attributes such as:
8485
- `include_pscr`
8586
- `tax_rate_percent`
8687
- `pscr_cents`
88+
- `current_rate_formula`
89+
- `current_rate_calculation`
8790
- `selected_rate_available`
8891
- `warning` (when selected plan disappears)
8992

custom_components/dte_rates/config_flow.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,5 +134,6 @@ def _rider18_status(rate_card) -> str:
134134
def _tax_note() -> str:
135135
return (
136136
"Default tax is Michigan's 4.0% residential electric sales tax. "
137+
"Detroit residents may incur an additional 5.0% City Utility Users' Tax. "
137138
"Set tax rate to 0 to omit taxes from rate calculations."
138139
)

custom_components/dte_rates/const.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
ATTR_SELECTED_RATE_AVAILABLE = "selected_rate_available"
4242
ATTR_WARNING = "warning"
4343
ATTR_CURRENT_RATE_NAME = "current_rate_name"
44+
ATTR_CURRENT_RATE_CALCULATION = "current_rate_calculation"
45+
ATTR_CURRENT_RATE_FORMULA = "current_rate_formula"
4446
ATTR_NEXT_RATE_CHANGE = "next_rate_change"
4547
ATTR_NEXT_RATE_NAME = "next_rate_name"
4648
ATTR_NEXT_RATE_VALUE = "next_rate_value"

custom_components/dte_rates/sensor.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
ATTR_COMPONENTS,
2020
ATTR_MONTHLY_COMPONENTS,
2121
ATTR_PERIOD,
22+
ATTR_CURRENT_RATE_CALCULATION,
23+
ATTR_CURRENT_RATE_FORMULA,
2224
ATTR_CURRENT_RATE_NAME,
2325
ATTR_EXPORT_RATE_SOURCE,
2426
ATTR_EXPORT_RATE_WARNING,
@@ -46,6 +48,7 @@
4648
)
4749
from .models import RatePlan, SeasonalPeriodRate
4850
from .rate_calculator import (
51+
GENERATION_COMPONENT_MARKERS,
4952
current_export_rate_cents,
5053
current_import_rate_cents,
5154
get_active_period,
@@ -161,6 +164,80 @@ def _export_rate_warning(self, period: SeasonalPeriodRate) -> str | None:
161164
"for the active period; using the available formula components only."
162165
)
163166

167+
def _import_rate_calculation(self, period: SeasonalPeriodRate, mode: str = "import") -> dict:
168+
base = period.components.per_kwh_total
169+
pscr = self._pscr_cents_for_rate()
170+
pscr_applied = pscr if self._include_pscr() and pscr is not None else Decimal("0")
171+
pre_tax = base + pscr_applied
172+
tax_rate = self._tax_rate_percent()
173+
tax = pre_tax * tax_rate / Decimal("100")
174+
total = pre_tax + tax
175+
176+
formula = (
177+
f"((base {self._fmt(base)} + PSCR {self._fmt(pscr_applied)}) "
178+
f"* (1 + tax {self._fmt(tax_rate)}%)) / 100 = ${self._fmt_usd(total)}/kWh"
179+
)
180+
if mode == "net_metering_export":
181+
formula = f"net metering export uses modified import rate: {formula}"
182+
183+
return {
184+
"mode": mode,
185+
"base_cents_per_kwh": self._as_float(base),
186+
"pscr_cents_per_kwh": self._as_float(pscr),
187+
"pscr_included": self._include_pscr() and pscr is not None,
188+
"tax_rate_percent": self._as_float(tax_rate),
189+
"tax_applied": tax_rate != 0,
190+
"pre_tax_cents_per_kwh": self._as_float(pre_tax),
191+
"tax_cents_per_kwh": self._as_float(tax),
192+
"total_cents_per_kwh": self._as_float(total),
193+
"total_usd_per_kwh": self._as_float(total / Decimal("100")),
194+
"formula": formula,
195+
}
196+
197+
def _export_rate_calculation(self, period: SeasonalPeriodRate) -> dict:
198+
if self._entry.data.get(CONF_NET_METERING, False):
199+
return self._import_rate_calculation(period, "net_metering_export")
200+
201+
generation = sum(
202+
value
203+
for key, value in period.components.per_kwh.items()
204+
if any(marker in key for marker in GENERATION_COMPONENT_MARKERS)
205+
)
206+
pscr = self._pscr_cents_for_rate()
207+
pscr_applied = pscr if self._include_pscr() and pscr is not None else Decimal("0")
208+
total = generation + pscr_applied
209+
formula = (
210+
f"(generation {self._fmt(generation)} + PSCR {self._fmt(pscr_applied)}) "
211+
f"/ 100 = ${self._fmt_usd(total)}/kWh"
212+
)
213+
214+
return {
215+
"mode": "rider18_export",
216+
"generation_cents_per_kwh": self._as_float(generation),
217+
"pscr_cents_per_kwh": self._as_float(pscr),
218+
"pscr_included": self._include_pscr() and pscr is not None,
219+
"tax_rate_percent": self._as_float(self._tax_rate_percent()),
220+
"tax_applied": False,
221+
"total_cents_per_kwh": self._as_float(total),
222+
"total_usd_per_kwh": self._as_float(total / Decimal("100")),
223+
"formula": formula,
224+
}
225+
226+
def _period_calculation(self, period: SeasonalPeriodRate) -> dict:
227+
return self._import_rate_calculation(period)
228+
229+
@staticmethod
230+
def _as_float(value: Decimal | None) -> float | None:
231+
return float(value) if value is not None else None
232+
233+
@staticmethod
234+
def _fmt(value: Decimal) -> str:
235+
return f"{value:.4f}"
236+
237+
@staticmethod
238+
def _fmt_usd(cents: Decimal) -> str:
239+
return f"{(cents / Decimal('100')):.6f}"
240+
164241
def _warning(self) -> str | None:
165242
selected = self._entry.data[CONF_SELECTED_RATE]
166243
if selected not in self.coordinator.data.rates:
@@ -212,6 +289,9 @@ def _base_attributes(self) -> dict:
212289
ATTR_MONTHLY_COMPONENTS: {k: float(v) for k, v in period.components.monthly.items()},
213290
}
214291
)
292+
calculation = self._period_calculation(period)
293+
attrs[ATTR_CURRENT_RATE_CALCULATION] = calculation
294+
attrs[ATTR_CURRENT_RATE_FORMULA] = calculation["formula"]
215295
return attrs
216296

217297
def _period_value_usd(self, period: SeasonalPeriodRate | None) -> float | None:
@@ -348,6 +428,9 @@ def _period_value_usd(self, period: SeasonalPeriodRate | None) -> float | None:
348428
return None
349429
return float(self._export_rate_cents(period) / 100)
350430

431+
def _period_calculation(self, period: SeasonalPeriodRate) -> dict:
432+
return self._export_rate_calculation(period)
433+
351434

352435
class DteCurrentRateNameSensor(_DteBaseRateSensor):
353436
_attr_native_unit_of_measurement = None

docs/research/issue-3-rate-modifiers.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ The issue asks for rate modifiers so Home Assistant can show a closer out-of-poc
1717
- Users can disable PSCR with an `include_pscr` setting. When disabled, Rider 18 export credits intentionally use generation-only formula components and should not report a missing-PSCR warning.
1818
- The default tax rate is `4.0%`. Michigan Treasury describes residential electricity as taxed at a 4% rate, and MPSC residential bill-charge guidance says utility companies collect 4% sales tax from residential customers.
1919
- The tax rate remains free-form as `tax_rate`, labeled as a percentage. Users can enter `0` to omit taxes from rate calculations.
20+
- Detroit residents may incur an additional `5.0%` City Utility Users' Tax. The UI note mentions this but does not change the default, because the integration does not currently know the service address.
21+
- Rate entities expose `current_rate_formula` and `current_rate_calculation` attributes so users can see the PSCR/tax inputs and exact math used for the active period.
2022

2123
## Calculation Scope
2224

@@ -28,3 +30,4 @@ The issue asks for rate modifiers so Home Assistant can show a closer out-of-poc
2830

2931
- Michigan Treasury sales and use tax page: https://www.michigan.gov/taxes/business-taxes/sales-use-tax
3032
- MPSC residential electric bill charges PDF: https://www.michigan.gov/-/media/Project/Websites/mpsc/consumer/info/tips/electric_residential_bill_charges_final.pdf?rev=e80ef0b163614800b1d8910b9e781775
33+
- City of Detroit September 2019 Revenue Estimating Conference Report: https://detroitmi.gov/sites/detroitmi.localhost/files/migrated_docs/financial-reports/Sept2019RevenueEstimatingConferenceReportFINAL.pdf

tests/test_config_flow.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ async def _refresh(self):
8585
== "Rider 18 export credits use parsed generation rates plus MPSC PSCR factors loaded for 2 tariffs."
8686
)
8787
assert "Set tax rate to 0" in result["description_placeholders"]["tax_note"]
88+
assert "Detroit residents may incur an additional 5.0%" in result["description_placeholders"]["tax_note"]
8889

8990

9091
@pytest.mark.asyncio

tests/test_sensor.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,20 @@ def test_import_sensor_returns_default_out_of_pocket_rate(monkeypatch):
6868
assert attrs["next_rate_change"] is None
6969
assert attrs["include_pscr"] is True
7070
assert attrs["tax_rate_percent"] == 4.0
71+
assert attrs["current_rate_formula"] == "((base 6.0000 + PSCR 1.8770) * (1 + tax 4.0000%)) / 100 = $0.081921/kWh"
72+
assert attrs["current_rate_calculation"] == {
73+
"mode": "import",
74+
"base_cents_per_kwh": 6.0,
75+
"pscr_cents_per_kwh": 1.877,
76+
"pscr_included": True,
77+
"tax_rate_percent": 4.0,
78+
"tax_applied": True,
79+
"pre_tax_cents_per_kwh": 7.877,
80+
"tax_cents_per_kwh": 0.31508,
81+
"total_cents_per_kwh": 8.19208,
82+
"total_usd_per_kwh": 0.0819208,
83+
"formula": "((base 6.0000 + PSCR 1.8770) * (1 + tax 4.0000%)) / 100 = $0.081921/kWh",
84+
}
7185

7286

7387
def test_import_sensor_can_omit_modifiers(monkeypatch):
@@ -105,6 +119,11 @@ def test_export_sensor_uses_rider18_formula_without_net_metering(monkeypatch):
105119
assert sensor.extra_state_attributes["rider18_export_available"] is True
106120
assert sensor.extra_state_attributes["pscr_cents"] == 1.877
107121
assert sensor.extra_state_attributes["pscr_rate_code"] == "D1.11"
122+
assert (
123+
sensor.extra_state_attributes["current_rate_formula"]
124+
== "(generation 3.0000 + PSCR 1.8770) / 100 = $0.048770/kWh"
125+
)
126+
assert sensor.extra_state_attributes["current_rate_calculation"]["tax_applied"] is False
108127

109128

110129
def test_export_sensor_ignores_rider18_credit_with_net_metering(monkeypatch):
@@ -116,6 +135,8 @@ def test_export_sensor_ignores_rider18_credit_with_net_metering(monkeypatch):
116135
sensor = DteExportRateSensor(coordinator, entry)
117136
assert sensor.native_value == pytest.approx(0.0819208)
118137
assert sensor.extra_state_attributes["export_rate_source"] == "net_metering"
138+
assert sensor.extra_state_attributes["current_rate_calculation"]["mode"] == "net_metering_export"
139+
assert "net metering export" in sensor.extra_state_attributes["current_rate_formula"]
119140

120141

121142
def test_export_sensor_reports_formula_unavailable_without_pscr(monkeypatch):

0 commit comments

Comments
 (0)