Include solar export credit in the TOU cost breakdown
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 19s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 19s
Previously only showed import cost by tariff period; now also shows the feed-in/export credit components alongside it, plus a net (usage cost minus export credit) total. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
1dbfe3007b
commit
918ea39e8d
3 changed files with 52 additions and 15 deletions
|
|
@ -125,8 +125,9 @@ def usage_summary(days: str = Query("30")) -> dict:
|
|||
@router.get("/tou_breakdown")
|
||||
def tou_breakdown(days: str = Query("1")) -> dict:
|
||||
"""Cost breakdown by the current plan's own usage components (e.g. Peak /
|
||||
Off-Peak / Shoulder), so you can see where your spend is actually
|
||||
landing rather than just the headline total."""
|
||||
Off-Peak / Shoulder) and feed-in components (solar export credit), so
|
||||
you can see where your spend -- and your credit -- is actually landing
|
||||
rather than just the headline total."""
|
||||
end = datetime.now(timezone.utc)
|
||||
with get_session() as session:
|
||||
start = _resolve_start(session, days, end)
|
||||
|
|
@ -137,16 +138,28 @@ def tou_breakdown(days: str = Query("1")) -> dict:
|
|||
|
||||
readings = [IntervalReading(ts=row.ts, import_kwh=row.import_kwh, export_kwh=row.export_kwh) for row in rows]
|
||||
result = evaluate(current_plan, readings)
|
||||
components = [
|
||||
usage_components = [
|
||||
{"label": c.label, "kwh": round(c.kwh, 2), "cost_dollars": round(c.cost_dollars, 2)}
|
||||
for c in result.usage_breakdown
|
||||
if c.kwh > 1e-9
|
||||
]
|
||||
# feed_in_breakdown's cost_dollars is already negative (a credit), same
|
||||
# sign convention the plan comparison table uses.
|
||||
feed_in_components = [
|
||||
{"label": c.label, "kwh": round(c.kwh, 2), "cost_dollars": round(c.cost_dollars, 2)}
|
||||
for c in result.feed_in_breakdown
|
||||
if c.kwh > 1e-9
|
||||
]
|
||||
total_usage_cost = round(sum(c["cost_dollars"] for c in usage_components), 2)
|
||||
total_feed_in_credit = round(sum(c["cost_dollars"] for c in feed_in_components), 2)
|
||||
return {
|
||||
"available": True,
|
||||
"days_with_data": result.days_with_data,
|
||||
"components": sorted(components, key=lambda c: c["cost_dollars"], reverse=True),
|
||||
"total_usage_cost_dollars": round(sum(c["cost_dollars"] for c in components), 2),
|
||||
"usage_components": sorted(usage_components, key=lambda c: c["cost_dollars"], reverse=True),
|
||||
"feed_in_components": sorted(feed_in_components, key=lambda c: c["cost_dollars"]),
|
||||
"total_usage_cost_dollars": total_usage_cost,
|
||||
"total_feed_in_credit_dollars": total_feed_in_credit,
|
||||
"net_usage_and_export_dollars": round(total_usage_cost + total_feed_in_credit, 2),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -202,7 +202,9 @@ form.settings-form label.p-day-label input {
|
|||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tou-bar-fill { height: 100%; background: var(--accent); border-radius: 5px; }
|
||||
.tou-bar-fill { height: 100%; border-radius: 5px; }
|
||||
.tou-bar-fill-cost { background: var(--accent); }
|
||||
.tou-bar-fill-credit { background: var(--good); }
|
||||
.tou-bar-amount { width: 130px; flex-shrink: 0; text-align: right; }
|
||||
|
||||
.pill-toggle { display: inline-flex; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
|
||||
|
|
|
|||
|
|
@ -262,21 +262,43 @@ async function loadDashboard(days) {
|
|||
});
|
||||
}
|
||||
|
||||
function touBarRows(components, maxAbsCost, fillClass) {
|
||||
return components.map(c => `
|
||||
<div class="tou-bar-row">
|
||||
<div class="tou-bar-label">${c.label}</div>
|
||||
<div class="tou-bar-track"><div class="tou-bar-fill ${fillClass}" style="width:${Math.max(2, 100 * Math.abs(c.cost_dollars) / maxAbsCost)}%;"></div></div>
|
||||
<div class="tou-bar-amount">${c.cost_dollars < 0 ? '-' : ''}$${Math.abs(c.cost_dollars).toFixed(2)} · ${c.kwh.toFixed(1)} kWh</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function loadTouBreakdown(days) {
|
||||
const body = document.getElementById('tou-breakdown-body');
|
||||
const data = await fetch(`/api/usage/tou_breakdown?days=${days}`).then(r => r.json());
|
||||
if (!data.available || !data.components.length) {
|
||||
if (!data.available || (!data.usage_components.length && !data.feed_in_components.length)) {
|
||||
body.innerHTML = '<div class="muted">No usage cost data yet for this period.</div>';
|
||||
return;
|
||||
}
|
||||
const maxCost = Math.max(...data.components.map(c => c.cost_dollars), 0.01);
|
||||
body.innerHTML = data.components.map(c => `
|
||||
<div class="tou-bar-row">
|
||||
<div class="tou-bar-label">${c.label}</div>
|
||||
<div class="tou-bar-track"><div class="tou-bar-fill" style="width:${Math.max(2, 100 * c.cost_dollars / maxCost)}%;"></div></div>
|
||||
<div class="tou-bar-amount">$${c.cost_dollars.toFixed(2)} · ${c.kwh.toFixed(1)} kWh</div>
|
||||
</div>
|
||||
`).join('') + `<div class="muted" style="margin-top:0.6rem;">Total usage cost: $${data.total_usage_cost_dollars.toFixed(2)} over ${data.days_with_data} day(s) — excludes supply charge and feed-in credit.</div>`;
|
||||
const maxAbsCost = Math.max(
|
||||
...data.usage_components.map(c => Math.abs(c.cost_dollars)),
|
||||
...data.feed_in_components.map(c => Math.abs(c.cost_dollars)),
|
||||
0.01
|
||||
);
|
||||
|
||||
const usageHtml = data.usage_components.length
|
||||
? `<div class="muted" style="margin-top:0.5rem;">Usage cost</div>${touBarRows(data.usage_components, maxAbsCost, 'tou-bar-fill-cost')}`
|
||||
: '';
|
||||
const feedInHtml = data.feed_in_components.length
|
||||
? `<div class="muted" style="margin-top:0.75rem;">Solar export credit</div>${touBarRows(data.feed_in_components, maxAbsCost, 'tou-bar-fill-credit')}`
|
||||
: '';
|
||||
|
||||
const net = data.net_usage_and_export_dollars;
|
||||
body.innerHTML = usageHtml + feedInHtml + `
|
||||
<div class="muted" style="margin-top:0.6rem;">
|
||||
Usage cost $${data.total_usage_cost_dollars.toFixed(2)} − export credit $${Math.abs(data.total_feed_in_credit_dollars).toFixed(2)}
|
||||
= <strong class="${net >= 0 ? 'savings-negative' : 'savings-positive'}">${net >= 0 ? '$' : '-$'}${Math.abs(net).toFixed(2)}</strong> net
|
||||
over ${data.days_with_data} day(s) — excludes supply charge.
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const touToggle = document.getElementById('tou-range-toggle');
|
||||
|
|
|
|||
Loading…
Reference in a new issue