Skip to content

Add energy/cost toggle to energy device cards#51687

Draft
V0lantis wants to merge 2 commits into
home-assistant:devfrom
V0lantis:energy/per-device-cost-toggle
Draft

Add energy/cost toggle to energy device cards#51687
V0lantis wants to merge 2 commits into
home-assistant:devfrom
V0lantis:energy/per-device-cost-toggle

Conversation

@V0lantis
Copy link
Copy Markdown

@V0lantis V0lantis commented Apr 23, 2026

Proposed change

Add an Energy ↔ Cost toggle to the two "Individual devices" cards on the energy dashboard, so you can see at a glance how much each device is costing rather than just its kWh.

The toggle is shared across both cards (and persisted across reloads) via the existing @storage({ subscribe: true }) decorator — no changes to the energy collection wiring.

Per-device cost is computed in the frontend only, no backend change. For each statistics bucket: device_cost(h) = device_kWh(h) × (Σgrid_cost(h) / Σgrid_kWh(h)). Multiple grid sources sum first, so a user with two grid imports gets a single ratio per hour. Untracked consumption is included in cost mode: per-hour where the bucket has a grid ratio, period-average where it doesn't (typically hours fully covered by solar). Helpers and unit tests live in src/data/energy_device_cost.ts.

The toggle only appears when a grid cost source is configured and hass.config.currency is set, so Intl.NumberFormat({ style: "currency" }) never gets an empty code.

Screenshots

Added below.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New feature (thank you!)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

Design notes / known limitations

  • Devices on self-produced solar show 0 cost per hour. The ratio = grid_cost / grid_kWh formula only prices grid imports; a device running entirely off solar that hour gets a strict 0. Deliberate.
  • Untracked uses a period-average fallback. Untracked residual energy is often consumed during the very hours where grid_kWh = 0. A strict per-hour ratio would drop those buckets to 0 and the untracked series would mostly disappear in cost mode. Falling back to the period-average rate keeps the series visible, at the cost of slight inaccuracy for ToU/dynamic tariffs.
  • Sidesteps the 2022 multi-grid objection. The earlier attempt (Parent grid option for individual device and per device cost calculation #13396, core#76925) got stuck in architecture#790 on the "parent grid" concept. This PR never allocates devices to grids — every grid source's cost and kWh are aggregated first, then one ratio is applied, so balloob's point that HA doesn't model multiple grid networks no longer applies.

Checklist

  • I understand the code I am submitting and can explain how it works.
  • The code change is tested and works locally.
  • There is no commented out code in this PR.
  • I have followed the perfect PR recommendations
  • Any generated code has been carefully reviewed for correctness and compliance with project standards.

If user exposed functionality or configuration variables are added/changed:

To help with the load of incoming pull requests:

Add a toggle on the individual devices summary and detail graph cards
that switches the displayed unit between kWh and the configured currency.

Per-device cost is computed in the frontend only: for each statistics
bucket, device_kWh(h) * (grid_cost(h) / grid_kWh(h)). Helpers and unit
tests live in src/data/energy_device_cost.ts. No backend change.

Toggle state is shared across all cards (and persisted) via
@storage({ subscribe: true }). When no grid cost data is present the
toggle stays, with a tooltip explaining why, and cost selection reverts
to energy.

Untracked-consumption bars are hidden in cost mode (they'd otherwise
double-count since grid cost is already correct at aggregate level).
Solar self-consumption is not allocated; devices running on self-
produced solar still show a grid-priced cost. Both documented.
Copy link
Copy Markdown

@home-assistant home-assistant Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @V0lantis

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!

@home-assistant home-assistant Bot marked this pull request as draft April 23, 2026 14:05
@home-assistant
Copy link
Copy Markdown

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

Show the untracked residual in cost mode priced per-hour (falls back to
the period-average rate for buckets where the per-hour grid import was
zero — typically solar-covered hours). This replaces the earlier "hide
untracked in cost mode" simplification, which left the chart looking
empty for users whose consumption is mostly untracked.

Other review fixes:
- Guard the toggle on hass.config.currency to avoid a RangeError when
  Intl.NumberFormat({ style: "currency" }) gets an empty code
- Hide the toggle entirely when cost is unavailable (previously a
  phantom "Cost" button would flip and flip back, flickering)
- Drop the dead deviceCostSeries helper, superseded by the post-process
  ratio scaling in the detail card
- Pre-index per-device stats into Map<start, change> so the untracked
  cost computation is O(buckets * devices) rather than O(buckets *
  devices^2) on long windows
- Unify the two cards' header layout: shared .header-actions wrapper,
  conditional title rendering instead of a fragile :empty selector
- Apply the maximumFractionDigits: 3 precision bump only in energy
  mode; in cost mode defer to Intl's per-currency minimum fraction
  digits. Note this also fixes a pre-existing latent bug where
  params.value < 0.1 compared an array to a number and was always false
@karwosts
Copy link
Copy Markdown
Member

While I'm a bit skeptical of this approach (I still think this should probably get resolved via architecture), I will point out that if you want someone to review your PR you need to mark "Ready for review".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants