Skip to content

Data Sources and Math

cataseven edited this page Jul 29, 2026 · 1 revision

Data Sources and Math

Jump to

🎚️ On-Card Pickers (Points/Hour & Group By)

Change the data resolution and the bucketing strategy directly on the card — no editor round-trip.

Setup

type: custom:statistics-graph-chart-card
show_pph_picker: true
show_group_by_picker: true
group_by_picker_position: right
entities:
  - entity: sensor.power

Behavior

  • Each dropdown starts on Auto (…), showing your configured value; picking anything else overrides it for this card only.
  • Selections persist across reloads and take priority over Auto Scale while active.
  • The Group By picker offers Interval, Hour, 2H / 3H / 4H / 6H / 12H, Date, Week, Month and Year.

Group Sync

Each picker has its own, fully independent sync group — same mechanism as date_picker_group:

# Controller card
show_pph_picker: true
pph_picker_group: page

# Receiver cards — no picker shown, still follow
pph_picker_group: page

pph_picker_group and group_by_picker_group don't interact: a card can follow one page-wide resolution group and a different bucketing group (or none).

Editor

General Settings → Overlay tab → Card OverlaysPoints/Hour Picker and Group By Picker rows. The Group fields are always editable, even while the toggles are off.

🧲 Raw Grouping (group_by: raw)

group_by: raw turns bucketing off entirely: every recorded sample is drawn at its exact timestamp, with no averaging and no resampling — points_per_hour is ignored.

type: custom:statistics-graph-chart-card
group_by: raw
hours_to_show: 24
entities:
  - entity: binary_sensor.front_door
    name: Front Door
    graph_type: step
    state_map:
      - value: "off"
      - value: "on"

When to use it

  • Step charts of binary / state sensors — a door, pump, or heating-demand signal changes state at precise moments; bucketing smears those edges. Raw mode preserves every transition exactly where it happened.
  • Sensors that record few, meaningful samples where a value interpolated into buckets would be misleading.

Editor

The Group By dropdown offers "Raw (no grouping)" whenever at least one entity uses graph_type: step — and always when the config already contains group_by: raw.

🔀 Value Transform

Apply a JavaScript expression to every data point before graphing. The transform has access to the current value and series-level statistics, making it possible to normalize, compare, and reshape data in ways that weren't possible before.

Available variables

Variable Description
x Current data point value
first First value in the visible time window
last Last value in the visible time window
min Minimum value across the series
max Maximum value across the series
avg Average value across the series
index Position of the current point (0, 1, 2…)

All variables are computed after value_factor is applied, before the transform runs.

Normalize to zero

Display cumulative meter readings as relative consumption starting from zero:

entities:
  - entity: sensor.gas_meter
    value_transform: "return x - first;"

A gas meter reading 2200, 2210, 2225, 2240 becomes 0, 10, 25, 40.

Splitting a sensor into export/import

A common use case: a single power sensor reports positive values for export and negative values for import. Use two entity entries with different transforms to separate them:

entities:
  - entity: sensor.grid_power
    name: "Grid Export"
    color: "#2ecc71"
    value_transform: "return x > 0 ? x : 0"

  - entity: sensor.grid_power
    name: "Grid Import"
    color: "#e74c3c"
    value_transform: "return x < 0 ? -x : 0"

Common expressions

Expression What it does
return x - first Normalize to zero (cumulative → relative)
return ((x - first) / first) * 100 Percentage change from start
return (x - min) / (max - min) Min-max normalization (scale to 0–1)
return x - avg Deviation from average
return x > 0 ? x : 0 Keep only positive values (zero out negatives)
return x < 0 ? -x : 0 Keep only negative values, flip to positive
return Math.abs(x) Absolute value
return x * 1.1 Add 10% markup
return x - 273.15 Convert Kelvin to Celsius
return (x * 9/5) + 32 Convert Celsius to Fahrenheit
return Math.round(x / 100) * 100 Round to nearest hundred

Editor

Entity → Advanced tab → Advanced CalculationsValue Transform — a monospace text input field. Enter the expression directly (e.g., return x - first).

Notes

  • The expression must be valid JavaScript and include a return statement
  • If the expression throws or returns a non-number (or null), that data point is dropped from the series — it is not silently replaced by the original value
  • Applied to every data point individually — both historical and live values
  • Works with all chart modes, aggregation functions, and other entity options
  • Context variables (first, min, etc.) are only available when processing a full data series — in the state row live value display, all context variables equal x
💶 Cost View (price_entity)

Multiply a series by the value of another entity over time — turn a kWh consumption chart into a money chart using a dynamic price sensor (Nordpool, EPEX, Tibber, a utility tariff helper). Per-entity price_entity needs no template sensor and no duplicate entity: the card bills the consumption itself.

type: custom:statistics-graph-chart-card
card_header: Energy Cost
hours_to_show: 168
group_by: date
entities:
  - entity: sensor.energy_meter
    name: Cost
    graph_type: bar
    aggregate_func: change
    price_entity: sensor.electricity_price
    unit: ""
    decimals: 2

How it works

  • The price is read as a step function from the price entity's own history — each price is valid from the moment it was recorded until the next change. Long windows automatically use long-term statistics (hourly mean) for the price series.
  • The multiplication happens per consumption slice, before bucketing — every slice of consumption is billed at the price that was active at that moment, so each bucket is the exact sum Σ(valueᵢ × priceᵢ).
  • Designed for aggregate_func: change on cumulative energy counters — set unit to your currency so the state row, tooltip, and axis all read as money.
  • price_attribute reads an attribute of the price entity instead of its state — dot notation is supported for nested paths (e.g. raw_today.0.value).
  • A state change of the tariff entity auto-refreshes the card, so a price update redraws the cost immediately.
  • Period Comparison ghosts are billed at their own period's prices — last week's ghost uses last week's tariffs, keeping the comparison honest.
  • Not applied to fixed_value or data_attribute entities.

Why a step function beats average × total

Multiplying the period's total consumption by the average price is only correct when consumption is flat. With time-of-use tariffs the expensive hours tend to be exactly the high-consumption hours, so average × total systematically misstates the bill. Billing every slice at its own momentary price reproduces what the utility actually charges.

Editor

Entity → Advanced tab → Advanced CalculationsPrice Entity and Price Attribute inputs.

➖ Two-Entity Math (ref_entity)

Plot the combination of two entities — most often their difference — without creating a template sensor. ref_entity names the second entity and ref_op says how to combine them:

type: custom:statistics-graph-chart-card
hours_to_show: 24
entities:
  # indoor − outdoor, the classic delta. ref_op defaults to subtract,
  # and with no name: the series is labelled "Indoor − Outdoor" automatically.
  - entity: sensor.indoor_temperature
    ref_entity: sensor.outdoor_temperature

  - entity: sensor.solar_production
    name: Net export
    ref_entity: sensor.house_consumption
    ref_op: subtract

  # the reference can come from an attribute (e.g. a weather entity)
  - entity: sensor.pool_temperature
    ref_entity: weather.home
    ref_attribute: temperature
    ref_op: divide

How it works

The reference entity's own history is fetched alongside your data and read as a step function: every sample of the main entity is combined with the reference value that was valid at that moment. The math runs before bucketing, so avg over a bucket is the average of the difference, and min/max are the smallest/largest instantaneous gap — not min(A) − min(B).

Rules worth knowing

  • Reversed operators. reverse_subtract gives B − A and reverse_divide gives B ÷ A. This is not the same as swapping the two entity ids: the main entity supplies the timeline the reference is sampled onto, plus the unit, aggregation, data source, colour and offset. Keep the better-sampled sensor as entity: and flip the arithmetic instead (e.g. COP = heat ÷ power while power stays the main series).
  • subtract / add / reverse_subtract combine levels. They are refused when aggregate_func is change, diff, sum or delta — those are already per-bucket deltas, so subtracting a level from them is meaningless. The series renders empty and logs one console warning. Use a level aggregation (avg, min, max, last, first, median) instead. multiply / divide are allowed with every aggregation — that is exactly what price_entity does.
  • Carry-forward, but nothing invented. The reference is held until it next changes — the same rule Home Assistant itself uses for a state, and the same one price_entity follows. That is what makes a step-shaped reference (a setpoint, an input_number, a mode) work. What the card will not do is invent a value before the reference's first sample: that stretch is a gap. Since the card carries values across gaps by default, a reference that dies mid-window shows as a flat line — add break_on_null: true if you would rather the line break there.
  • Both operands should share a unit for subtract/add; the result keeps the main entity's unit. For multiply/divide set unit yourself.
  • value_factor and invert apply to the main entity only, before the combination.
  • The state row and the trend arrow follow the combined series, not the bare main entity.
  • Not available on data_attribute (forecast-array) or fixed_value rows.
  • Only one reference and one operator per entity. For multi-operand formulas (A − B − C) you still need a template sensor — a general cross-entity expression is planned for a later release.

Note: data_value_expression + data_vars do not do this. They only apply to an attribute-array data source, and data_vars resolves each name to the referenced entity's current state — a constant, not a series. Use ref_entity for math against another entity over time.

Editor: Entity → AdvancedReference Entity, Operation, Reference Attribute.

📏 Range Band

The Range Band feature draws a shaded min/max area behind each line entity, showing how much the value fluctuated within each aggregation bucket.

Range Band Example

How it works

When points_per_hour aggregates multiple raw data points into a single graph point, the displayed value is typically the average (or whichever aggregate_func you've chosen). The range band shows the full min → max spread of raw values that were combined — so you can see both the trend and the volatility.

entities:
  - entity: sensor.outdoor_temperature
    show_range_band: true
    color: "#ff4757"
  - entity: sensor.indoor_temperature
    show_range_band: false
    color: "#378ADD"

Use cases

  • Temperature: narrow band = stable climate, wide band = fluctuating (e.g. HVAC cycling)
  • Energy: see consumption spikes vs steady draw within each time bucket
  • Sensor noise: distinguish real signal changes from noisy sensor readings

Editor

Entity → Graph tab → Line section → Range Band toggle (next to Data Points). Timeline mode only.

Tooltip

When hovering, an additional row shows the range: Range: 21.2 → 22.8 °C.

〽️ Moving Averages

Overlay one or more simple moving-average (SMA) lines on top of any entity — a smoothed trend line that averages the last N buckets. Add as many as you like, each with its own period, color, and width. Perfect for separating signal from noise on busy sensors (CPU load, power, prices) or for classic short/long crossover views (e.g. MA7 vs MA26).

Moving Averages Example

entities:
  - entity: sensor.keenetic_router_cpu_load
    graph_type: bar
    moving_averages:
      - period: 7          # short MA — averages the last 7 buckets
        color: "#ffffff"
        show_label: true   # draw a small "MA7" tag on the line
      - period: 26         # long MA — averages the last 26 buckets
        color: "#f1c40f"
        width: 2           # optional line thickness (px)
        show_label: true   # draw "MA26"

The period is in buckets, not hours

Each line's period is a number of buckets, and the bucket size comes from the chart's timeframe — points_per_hour in interval mode, or group_by (hour / date / week / month / year). So with group_by: hour, period: 26 averages the last 26 hourly buckets (MA26). With points_per_hour: 12 (one bucket every 5 minutes), period: 26 covers the last ~130 minutes.

Automatic look-back — long MAs draw on short windows

A 26-bucket average normally needs 26 buckets of history before it can produce its first value. If your visible window only holds a handful of buckets (e.g. hours_to_show: 24 with group_by: hour → 24 buckets), a plain MA26 would never have enough data to draw.

This card solves that by extending the history fetch backward automatically by the amount the longest moving average needs — independent of the visible window. The MA is computed over that extended range and then trimmed to the window, so MA26 draws fully across a 24-hour view without you having to widen hours_to_show or scroll. The visible chart, axis, statistics, and extrema are unchanged — only the moving-average lines use the extra look-back.

The look-back applies to entities whose data comes from History or long-term statistics. Synthetic sources (fixed_value, data_attribute) average only what's in the window.

Candlesticks

When the entity is drawn as candlesticks, each moving average uses the close of every candle (otherwise it averages the bucket value), so an MA over candles behaves like the moving averages on a trading chart.

Line labels

Turn on show_label for any line to draw a small MA7, MA26 … tag right at the end of that line, so you can tell several averages apart at a glance:

    moving_averages:
      - period: 7
        color: "#ffffff"
        show_label: true

The label text is the period prefixed with MA (MA7, MA26, …). It's drawn at the end of the line and clamped inside the chart, so it never spills outside the plot, with a subtle background halo so it stays readable over the line and the data. It's off by default — flip the Label switch next to the color picker, or set show_label: true in YAML.

Notes

  • Each moving average is plotted against the entity's own Y axis, so it lines up with the bars / line / candles it summarizes.
  • Lines render in their configured color; leave color empty to fall back to a default. width defaults to a thin line.
  • Timeline mode only.

Editor

Per-entity → Graph tab → Moving Averages (just below the Graph card). Click Add Moving Average to add a line, set its Period and Color, flip Label to print an MA7-style tag on the line, and use the button to remove one.

🕳️ Break on Gaps

When a sensor goes unavailable or unknown, the card normally keeps the line continuous by carrying the last known value forward across the gap. That's great for short blips (brief WiFi drops, bucket-level reporting irregularity) but can hide genuine multi-hour outages behind a flat line. The per-entity break_on_null option lets you opt in to a visible break instead.

entities:
  - entity: sensor.steady_sensor
    # default — continuous line, last known value carried across any gap

  - entity: sensor.flakey_sensor
    break_on_null: true
    # short blips still connect, but longer outages appear as gaps

  - entity: sensor.very_specific
    break_on_null: true
    carry_forward_ms: 900000   # advanced — 15 min threshold

Behavior

Setting Effect
break_on_null: false (default) Carry-forward runs indefinitely within the visible window. No null-induced gaps, ever. The long-standing default behavior.
break_on_null: true Short sample gaps (bucket-level blips, irregular reporting, brief drops) stay connected. Longer outages — default threshold min(3 × bucket width, 30 minutes) — appear as visible breaks in the line.
carry_forward_ms: N (YAML, advanced) Override the threshold with an explicit value in milliseconds. Applies regardless of the break_on_null setting.

Why not just "break at every null"?

A pure "zero carry-forward" mode turns out to be too aggressive on real-world sensors. When bucket width is finer than the sensor's natural sample interval (a common setup at high points_per_hour), dozens of buckets between two real samples end up empty even though the sensor is perfectly healthy. The time-based threshold distinguishes "I'm waiting for the next sample" from "the device has been gone for a while" — which is the distinction users actually want to see.

Editor

Entity → Graph tab → Graph section → Break on Gaps toggle (next to Show Average and Data Labels). Shown in every chart mode — carry-forward affects the bucketed data, so it matters wherever those buckets are drawn.

Since v3.32 the editor hides options that do nothing for the selected chart mode, down to the individual field — a section disappears once everything inside it is hidden, a tab once all its sections are, and you are moved to the first surviving tab. Break on Gaps survives that pass in all fourteen modes, while a Timeline-only neighbour like Range Band does not. Nothing is lost either way: a hidden option keeps its YAML value and comes back when you switch modes again.

Notes

  • Changed in v3.25 — an empty bucket now carries the previous bucket's last recorded sample (its exit state) forward, not the previous bucket's aggregated value. With aggregate_func: first or max, a momentary spike no longer smears across the quiet buckets that follow it; with avg, the carried value is the last actual reading instead of the previous bucket's average.
  • Does not affect value_transform scripts that return null to drop a bucket — those nulls are removed from the series before carry-forward logic runs.
  • Works independently per entity, so you can mix well-behaved sensors (continuous line) with flakey ones (visible gaps) on the same card.
📡 Attribute Data Source

Read chart data directly from an entity attribute instead of history. The attribute must contain an array of objects with time and value fields. The X-axis automatically extends into the future when data contains future timestamps.

Ideal for energy spot prices, weather forecasts, and solar production predictions. No history or statistics API calls are made for these entities.

entities:
  - entity: sensor.epex_spot_price
    data_attribute: data
    data_time_field: start_time
    data_value_field: price_per_kwh
    value_factor: 2
    graph_type: step
    name: "Electricity Price"

Common configurations:

Integration data_attribute data_time_field data_value_field
EPEX Spot data start_time price_per_kwh
Nordpool raw_today start value
Tibber price_info startsAt total
Forecast.Solar detailedForecasts period_start pv_estimate

Compatible with existing value_factor, value_transform, aggregate_func, and group_by. The time and value field names support nested paths via dot notation (e.g. forecast.0.temperature).

Computed Values

Sometimes the value you want isn't a single field — you need to combine several fields from each element, fold in other entities (fees, tax rates, tariffs), and apply a formula. Set data_value_expression instead of data_value_field: a small arithmetic expression evaluated for every array element, with the element's own fields in scope plus any names you map in data_vars.

entities:
  - entity: sensor.tibber_hourly_cost
    name: Electricity
    graph_type: bar
    data_attribute: nodes
    data_time_field: from
    data_time_unit: iso
    data_value_expression: "(unitPrice - unitPriceVAT) * 100 * consumption * (1 + mwst)"
    data_vars:
      mwst: input_number.vat_rate

data_vars maps a name to an entity ID; each resolves to that entity's numeric state and is re-evaluated live when it changes. Inside the expression you can use:

In scope Examples
Element fields unitPrice, consumption, … (any field of the array item)
data_vars names mwst, base, …
Operators + - * / % and parentheses ( )
Functions min, max, abs, round, floor, ceil, sqrt, pow

This is not JavaScript — there are no other variables, no property access, and no calls beyond those functions, so it can't run arbitrary code. If the expression is empty or fails to parse, the entity falls back to data_value_field.

Because each element is evaluated independently, two entities reading the same array with different expressions stack cleanly — e.g. splitting an hourly energy cost into a consumption layer and a fixed-fee layer:

type: custom:statistics-graph-chart-card
stacked: true
group_by: hour
entities:
  - entity: sensor.tibber_hourly_cost
    name: Energy
    graph_type: bar
    data_attribute: nodes
    data_time_field: from
    data_time_unit: iso
    data_value_expression: "(unitPrice - unitPriceVAT) * 100 * consumption * (1 + mwst)"
    data_vars: { mwst: input_number.vat_rate }
  - entity: sensor.tibber_hourly_cost
    name: Fees
    graph_type: bar
    data_attribute: nodes
    data_time_field: from
    data_time_unit: iso
    data_value_expression: "(base/30/24 + grid/24 + meter/24) * (1 + mwst) * 100"
    data_vars:
      base: input_number.base_price_month
      grid: input_number.grid_fee_day
      meter: input_number.meter_fee_day
      mwst: input_number.vat_rate

Editor: Per-entity → Advanced tab → Attribute Data SourceValue Expression field, and Data Vars (one name: entity_id per line).

Time Unit

By default, the time field is parsed as an ISO date string. Set data_time_unit to interpret it differently — perfect for sensors that expose monthly summaries, hourly profiles, day-of-year datasets, or Unix timestamps without generating artificial timestamps.

Unit Range Behavior
iso (default) string / epoch ms new Date(t) — existing behavior
epoch_seconds number Unix timestamp in seconds
epoch_ms number Unix timestamp in milliseconds
month_of_year 1..12 First day of that month in data_time_year (default: current year)
day_of_month 1..31 That day of the visible window's month, in data_time_year (default: the window's year). A value past the month's end is clamped to its last day
day_of_year 1..366 That day in data_time_year
week_of_year 1..53 Monday of that ISO week in data_time_year
hour_of_day 0..23 That hour of today

Example — a sensor exposing monthly yield expectations as [{Month: 1, Expectation: 320}, {Month: 2, Expectation: 489}, ...]:

type: custom:statistics-graph-chart-card
group_by: month
graph_start: year
hours_to_show: 8760
entities:
  - entity: sensor.sma_month_yield_expectation
    data_attribute: Expectation
    data_time_field: Month            # 1..12
    data_value_field: Expectation
    data_time_unit: month_of_year     # interpret as Jan..Dec
    graph_type: bar
    aggregate_func: max

Renders 12 bars labelled Jan–Dec on the X-axis. Tooltip shows the actual month + value. All other features (date picker, group_by, fill, theming) work normally because the parser converts the numeric category into a real Date internally.

Editor: Per-entity → Advanced tab → Attribute Data SourceTime Unit dropdown and Reference Year input

🔌 External Statistics

Display data from imported statistics that don't have a regular entity in Home Assistant. This covers energy data from integrations like Gazpar, Linky, Tibber, and others that import directly into HA's statistics database.

Background

Some HA integrations don't create sensor.* entities. Instead, they write data directly into the statistics and statistics_meta database tables using HA's async_import_statistics() API. These statistics have IDs with a colon separator (e.g. gazpar:gazpar_consumption) and are visible on the Energy dashboard and the built-in Statistics Graph card, but not in the entity registry.

Setup

Use statistic_id instead of (or alongside) entity:

entities:
  - entity: ""
    statistic_id: "gazpar:gazpar_consumption"
    name: "Gas Consumption"
    color: "#f39c12"
    aggregate_func: sum
  - entity: ""
    statistic_id: "linky:linky_consumption"
    name: "Electricity"
    color: "#378ADD"
    aggregate_func: sum
  - entity: sensor.indoor_temperature
    name: "Temperature"
    color: "#ff4757"

You can mix external statistics with regular entities on the same card.

How it works

  • The card detects external statistics by checking for a : in the statistic_id
  • Data is fetched via HA's recorder/statistics_during_period WebSocket API — the same API the Energy dashboard uses
  • Since there is no live state, the state row displays the last known value from the statistics data
  • All card features work: tooltip, legend, axes, stacking, offset, zoom, color thresholds, etc.

Editor

Entity → General → Statistic ID input field (monospace, below the entity picker). Set the entity picker to empty and fill in the statistic ID.

Finding your statistic IDs

  1. Go to Developer Tools → Statistics in HA
  2. Search for the integration name (e.g. "gazpar")
  3. The statistic ID is shown in the list (e.g. gazpar:gazpar_consumption)

Clone this wiki locally