Skip to content

Dashboard cards

Cards that put your parcels on a dashboard: what is on its way, from whom, and when it lands. Each one is pulled straight from the Parcel Aggregator's examples/dashboards/ folder when this site is built, so it always matches the version that is actually shipped. For notifications and other logic, see the automation cookbook.

Like the automations, they are carrier-agnostic: they read the aggregator's merged sensors and the canonical parcel fields, so a card keeps working when you add a carrier — no new rows to write.

Would rather not write YAML at all? Two community projects package all of this into a finished card that finds your sensors by itself — see ready-made parcel cards below.

Not running the aggregator?

These work per carrier too. Swap the aggregator's sensors for the carrier's own (sensor.postnl_incoming_parcels, …). The parcels attribute holds the same fields either way — see the contract. Several carriers also ship dashboard examples of their own, in that repo's examples/dashboards/ folder.

Where these go

Open your dashboard, ✏️ → + Add card → Manual, and paste over the contents. On a card that already exists: ⋮ → Edit → Show code editor.

Cards

Parcels by carrier

Markdown card showing the per-carrier counter breakdown for incoming, en-route-to-pickup-point and recently delivered parcels in one glance. Lives off the by_carrier attribute every aggregator summary sensor exposes.

# Markdown card showing the per-carrier counter breakdown for incoming,
# en-route-to-pickup-point and recently delivered parcels in one glance.
# Lives off the `by_carrier` attribute every aggregator summary sensor
# exposes.
#
# Iteration is driven by the carriers present in the data, so the card
# automatically adapts to which carrier integrations you actually have
# installed — there is no per-carrier setup.

type: markdown
title: Parcels by carrier
content: >-
  {% set incoming = state_attr('sensor.parcel_aggregator_incoming_parcels', 'by_carrier') or {} %}
  {% set pickup = state_attr('sensor.parcel_aggregator_awaiting_pickup', 'by_carrier') or {} %}
  {% set delivered = state_attr('sensor.parcel_aggregator_delivered_parcels', 'by_carrier') or {} %}
  {% set carriers = (incoming.keys() | list + pickup.keys() | list + delivered.keys() | list) | unique | list %}

  | Carrier | Incoming | At pickup point | Recently delivered |
  | --- | ---: | ---: | ---: |
  {%- for carrier in carriers %}
  | {{ carrier }} | {{ incoming.get(carrier, 0) }} | {{ pickup.get(carrier, 0) }} | {{ delivered.get(carrier, 0) }} |
  {%- endfor %}

View on GitHub

Next delivery

Next-delivery card across every installed carrier. The aggregator's next-delivery sensor exposes the earliest planned_from datetime as its native value (rendered relatively by HA: "in 3 hours" / "tomorrow 10:00") and the matching parcel under the parcel attribute — so the carrier badge comes for free.

# Next-delivery card across every installed carrier. The aggregator's
# next-delivery sensor exposes the earliest planned_from datetime as
# its native value (rendered relatively by HA: "in 3 hours" /
# "tomorrow 10:00") and the matching parcel under the `parcel` attribute — so the
# carrier badge comes for free.

type: entities
title: Next delivery
entities:
  - entity: sensor.parcel_aggregator_next_delivery
    name: Expected
    secondary_info: last-updated
  - type: attribute
    entity: sensor.parcel_aggregator_next_delivery
    attribute: parcel
    name: Parcel
    format: relative
  - type: custom:template-entity-row
    name: Carrier
    state: >-
      {% set p = state_attr('sensor.parcel_aggregator_next_delivery', 'parcel') %}
      {{ p.carrier if p else 'no active parcels' }}
  - type: custom:template-entity-row
    name: Sender
    state: >-
      {% set p = state_attr('sensor.parcel_aggregator_next_delivery', 'parcel') %}
      {{ p.sender if p and p.sender else '—' }}

# The two `custom:template-entity-row` rows need the Lovelace plugin
# `template-entity-row` from HACS. Without it, swap them for plain
# attribute rows that point at `parcel` and project the keys in a
# separate markdown card — the data is on the same attribute.

View on GitHub

Unified active parcels grid

Markdown card that renders every active incoming parcel from every carrier in one table, with carrier, sender (clickable when a tracking URL is available), status, and the expected delivery window. Conditional on count > 0 so it hides itself on empty days.

# Markdown card that renders every active incoming parcel from every
# carrier in one table, with carrier, sender (clickable when a tracking
# URL is available), status, and the expected delivery window.
# Conditional on count > 0 so it hides itself on empty days.

type: conditional
conditions:
  - condition: numeric_state
    entity: sensor.parcel_aggregator_incoming_parcels
    above: 0
card:
  type: markdown
  title: Active parcels
  content: >-
    {%- set labels = {
      'registered': 'registered',
      'in_transit': 'in transit',
      'out_for_delivery': 'out for delivery',
      'at_pickup_point': 'ready for collection',
      'delivered': 'delivered',
      'problem': 'problem',
      'returning': 'returning to sender',
      'unknown': 'status unknown'
    } -%}
    {%- set ns = namespace(rows=[]) -%}
    {%- for p in state_attr('sensor.parcel_aggregator_incoming_parcels', 'parcels') or [] -%}
      {%- set sender = p.sender or 'Unknown sender' -%}
      {%- set status = labels.get(p.status, p.status) -%}
      {%- set sender_cell = '[%s](%s)' | format(sender, p.url) if p.url else sender -%}
      {%- if p.planned_from and p.planned_to -%}
        {%- set expected = (as_timestamp(p.planned_from) | timestamp_custom('%a %-d %b %H:%M', true)) ~ '–' ~ (as_timestamp(p.planned_to) | timestamp_custom('%H:%M', true)) -%}
      {%- elif p.planned_from -%}
        {%- set expected = as_timestamp(p.planned_from) | timestamp_custom('%a %-d %b %H:%M', true) -%}
      {%- else -%}
        {%- set expected = '—' -%}
      {%- endif -%}
      {%- set ns.rows = ns.rows + ['| %s | %s | %s | %s |' | format(p.carrier, sender_cell, status, expected)] -%}
    {%- endfor -%}
    {{ '| Carrier | Sender | Status | Expected |\n| :-- | :-- | :-- | :-- |\n' ~ (ns.rows | join('\n')) }}

View on GitHub

Ready-made parcel cards

The snippets above are deliberately plain — they use cards Home Assistant already ships, so nothing extra has to be installed. If you would rather have a finished parcel card, two community projects build one on top of these same sensors and detect your carriers automatically:

  • HKI Parcels Card — by jonisnet

    Every carrier you run in one card, with tabs for in transit, delivered, sent and PostNL letterbox scans, a per-parcel delivery tracker, and a + Add parcel control for the account-less carriers. Visual editor, no YAML.

  • Package Tracker Card — by klaptafel

    Drop it on a dashboard with no configuration at all and it finds every parcel sensor you have, deduplicated when the same parcel arrives through two sources, with an expandable event timeline per package and filters per carrier, status or direction.

Install either through HACS → Custom repositories, category Dashboard.

Both are independent projects, built and maintained by their authors and not by this org. The credit for them is theirs — and so are the bug reports and feature requests, which belong in their own trackers rather than in a carrier repo here.

Plugins the snippets use

A snippet that needs more than a built-in card reaches for one of these two Lovelace plugins instead. They are helpers inside a card rather than cards of their own, also installed through HACS:

Plugin By What it adds
custom:auto-entities Thomas Lovén Fills a card's entity list from a filter instead of a hand-written list, so parcels that only exist tomorrow still show up
custom:template-entity-row Thomas Lovén Renders a template as an entities-card row — how you get a value that lives on an attribute onto its own line

Neither is required. Every snippet that uses one says in its comments what to fall back to, and the parcel data sits on the sensor's attributes either way.