|
19 | 19 | ATTR_COMPONENTS, |
20 | 20 | ATTR_MONTHLY_COMPONENTS, |
21 | 21 | ATTR_PERIOD, |
| 22 | + ATTR_CURRENT_RATE_CALCULATION, |
| 23 | + ATTR_CURRENT_RATE_FORMULA, |
22 | 24 | ATTR_CURRENT_RATE_NAME, |
23 | 25 | ATTR_EXPORT_RATE_SOURCE, |
24 | 26 | ATTR_EXPORT_RATE_WARNING, |
|
46 | 48 | ) |
47 | 49 | from .models import RatePlan, SeasonalPeriodRate |
48 | 50 | from .rate_calculator import ( |
| 51 | + GENERATION_COMPONENT_MARKERS, |
49 | 52 | current_export_rate_cents, |
50 | 53 | current_import_rate_cents, |
51 | 54 | get_active_period, |
@@ -161,6 +164,80 @@ def _export_rate_warning(self, period: SeasonalPeriodRate) -> str | None: |
161 | 164 | "for the active period; using the available formula components only." |
162 | 165 | ) |
163 | 166 |
|
| 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 | + |
164 | 241 | def _warning(self) -> str | None: |
165 | 242 | selected = self._entry.data[CONF_SELECTED_RATE] |
166 | 243 | if selected not in self.coordinator.data.rates: |
@@ -212,6 +289,9 @@ def _base_attributes(self) -> dict: |
212 | 289 | ATTR_MONTHLY_COMPONENTS: {k: float(v) for k, v in period.components.monthly.items()}, |
213 | 290 | } |
214 | 291 | ) |
| 292 | + calculation = self._period_calculation(period) |
| 293 | + attrs[ATTR_CURRENT_RATE_CALCULATION] = calculation |
| 294 | + attrs[ATTR_CURRENT_RATE_FORMULA] = calculation["formula"] |
215 | 295 | return attrs |
216 | 296 |
|
217 | 297 | def _period_value_usd(self, period: SeasonalPeriodRate | None) -> float | None: |
@@ -348,6 +428,9 @@ def _period_value_usd(self, period: SeasonalPeriodRate | None) -> float | None: |
348 | 428 | return None |
349 | 429 | return float(self._export_rate_cents(period) / 100) |
350 | 430 |
|
| 431 | + def _period_calculation(self, period: SeasonalPeriodRate) -> dict: |
| 432 | + return self._export_rate_calculation(period) |
| 433 | + |
351 | 434 |
|
352 | 435 | class DteCurrentRateNameSensor(_DteBaseRateSensor): |
353 | 436 | _attr_native_unit_of_measurement = None |
|
0 commit comments