Skip to main content

Command Palette

Search for a command to run...

Headlines in Home Assistant with a REST sensor, and how often to poll

Home Assistant news feed: one rest: sensor, a markdown card, an 8 am TTS.

Updated
•12 min read•View as Markdown
Headlines in Home Assistant with a REST sensor, and how often to poll
A
We build APITube, a news API that returns articles from 300,000+ sources across 177 countries and 59 languages as structured JSON, with sentiment, entities and topics already attached. Posts here are measured experiments, numbers included.

Home Assistant's REST sensor polls every 30 seconds by default. Point it at a news API and that is 2,880 requests a day for a card nobody looks at more than a few times an hour. A free news API key gives us 100 requests a day, so the default burns the day's budget in 50 minutes. This article sets up a news feed in Home Assistant: a rest: sensor that fetches a JSON news endpoint on a schedule, keeps the headlines in an entity attribute, and feeds one markdown card and one automation that reads the headlines out loud at 8 am. Then it measures how often the top headlines really change, so scan_interval is a number we can defend.

We polled the same "latest 10 English headlines" query once a minute for 150 minutes on 17 September 2026. On average 1.79 new headlines entered the top 10 per minute, 34% of one-minute polls came back identical to the minute before, and at a 15-minute interval every poll returned a card with nothing repeated from the previous one. Field selection cut the response for five headlines from 179,154 bytes to 3,655 bytes, which matters because Home Assistant writes those attributes to its database on every change.

This is for people who already run Home Assistant and can edit configuration.yaml. No HACS, no shell scripts, no restart: the REST integration reloads from Developer tools.

Takeaways

  • Default scan_interval: 30 = 2,880 requests/day; free tier = 100/day, so the key is exhausted after 50 minutes.
  • 1.79 new headlines per minute on average (median 1); 50 of 147 consecutive one-minute polls (34%) returned the same top 10 as the minute before.
  • At scan_interval: 900 (15 min) 0% of the card repeats between polls, and the card shows 100 of the 273 headlines that passed through in 2.5 hours.
  • fl=id,title,href,source.domain,published_at shrinks the 5-headline response 49×: 179,154 → 3,655 bytes.
  • language=en is silently ignored by the API; language.code=en is the filter that works.

Why a JSON API and not the RSS feed

The two most-linked community threads for this task both scrape RSS: one picks BBC XML apart with Jinja regex_findall inside a REST sensor, the other runs curl plus a Python script behind a command_line sensor and needs a full restart on every change. Both work until a publisher changes its XML.

Unlike an RSS feed, a JSON news API returns a results array that json_attributes can store as-is, which means the whole "parser" is one line of YAML. It also lets us filter by language, country, category and source quality in the query instead of in a template. We use APITube below because it is ours; disclosure done. Any JSON news API with a header key and a results list works the same way, only the parameter names change.

Step 1: the request, checked with curl

Get the query right in a terminal before it goes into YAML. fl is the field list, and it is the parameter that keeps the sensor small.

curl -s -G "https://api.apitube.io/v1/news/everything" \
  -H "X-API-Key: $APITUBE_API_KEY" \
  --data-urlencode "per_page=5" \
  --data-urlencode "language.code=en" \
  --data-urlencode "source.rank.opr.min=6" \
  --data-urlencode "sort.by=published_at" \
  --data-urlencode "sort.order=desc" \
  --data-urlencode \
    "fl=id,title,href,source.domain,published_at"

The response is 3.7 KB. Two of the five results:

{
  "status": "ok",
  "limit": 5,
  "page": 1,
  "results": [
    {
      "id": 3083478940,
      "title": "Infineon sells FRAM and NOR Flash...",
      "href": "https://www.electronicsweekly.com/news/...",
      "source": { "domain": "electronicsweekly.com" },
      "published_at": "2026-09-17T07:11:36.000Z"
    },
    {
      "id": 3083478888,
      "title": "VdL to outline under-age internet and AI...",
      "href": "https://www.electronicsweekly.com/news/...",
      "source": { "domain": "electronicsweekly.com" },
      "published_at": "2026-09-17T07:11:36.000Z"
    }
  ]
}

Three parameters deserve a sentence each:

  • source.rank.opr.min=6 drops low-ranked domains. Without it the "latest" list is whatever small site published a second ago. Swap it for source.country.code=gb or category.id=medtop:04000000 (economy) if the card should be narrower.
  • language.code=en. We first wrote language=en, got a status: ok and a list with Czech and Slovak headlines in it. The API does not reject unknown parameters; it ignores them.
  • fl=. Without it the same five headlines come back as 179,154 bytes, because every result carries the body, entities, sentiment and media. With it: 3,655 bytes, 49× less.

Five headlines are 179,154 bytes without field selection and 3,655 bytes with fl=, a 49× difference

That size is not a bandwidth concern. It is a recorder concern: Home Assistant stores an entity's attributes in its SQLite or MariaDB database every time they change, so a 179 KB attribute blob that changes on most of its 96 daily polls is up to 17 MB of database growth per day for a headlines card.

Step 2: the REST sensor

Put the key in secrets.yaml and add this to configuration.yaml:

# secrets.yaml
apitube_api_key: YOUR_API_KEY
# configuration.yaml
rest:
  - resource: https://api.apitube.io/v1/news/everything
    # 15 minutes, see the measurement below
    scan_interval: 900
    timeout: 20
    headers:
      X-API-Key: !secret apitube_api_key
    params:
      per_page: 5
      language.code: en
      source.rank.opr.min: 6
      sort.by: published_at
      sort.order: desc
      fl: id,title,href,source.domain,published_at
    sensor:
      - name: News headlines
        unique_id: news_headlines
        value_template: "{{ value_json.results | count }}"
        json_attributes:
          - results

The sensor's state is the number of headlines (5). The headlines themselves live in the results attribute as a list of five dicts. That split is deliberate: Home Assistant refuses any state longer than 255 characters (MAX_LENGTH_STATE_STATE = 255 in homeassistant/const.py), and if a template produces more it logs "State … is longer than 255, falling back to unknown" and sets the state to unknown. Attributes have no such limit, so lists go there.

timeout: 20 is not decoration. In our 150 polls the median response took 739 ms and the 90th percentile 1,018 ms, but the slowest took 13.9 s; with the default timeout: 10 the sensor would have gone unavailable for that minute.

json_attributes without json_attributes_path reads keys from the root object, so results becomes state_attr('sensor.news_headlines', 'results'). No JSONPath, no regex_findall.

Reload without a restart: Developer tools → YAML → REST entities and notify services. Then Developer tools → States → sensor.news_headlines should show state 5 and a results attribute.

Step 3: the markdown card

Add a markdown card to the dashboard. The template loops over the attribute:

type: markdown
title: Headlines
content: |
  {% for a in state_attr('sensor.news_headlines',
                         'results') %}
  **[{{ a.title }}]({{ a.href }})**<br>
  <small>{{ a.source.domain }} ·
  {{ relative_time(as_datetime(a.published_at)) }}
  ago</small>

  {% endfor %}

relative_time() turns the ISO timestamp into "12 minutes", and Home Assistant re-renders templates that use it once a minute, so the "ago" stays right between polls. The blank line before {% endfor %} is what separates the items; without it they run together in one paragraph. The <small> tag is fine; the markdown card allows HTML, just not JavaScript.

Title length is the one thing worth guarding. Across the 273 distinct headlines in our log the longest title was 133 characters and the median 74, so even a title-as-state sensor would have stayed under 255. One long title from a small site would still flip it to unknown, which is why the count is the state.

Step 4: read the headlines at 8 am

One automation with a time trigger and tts.speak. Pick your TTS entity (tts.google_translate_en_com, tts.piper, whatever the TTS integration created) and a media player.

automation:
  - alias: Morning headlines
    triggers:
      - trigger: time
        at: "08:00:00"
    actions:
      - action: tts.speak
        target:
          entity_id: tts.google_translate_en_com
        data:
          media_player_entity_id: media_player.kitchen
          message: >-
            Good morning. The top three headlines.
            {% for a in state_attr(
                'sensor.news_headlines',
                'results')[:3] %}
            {{ loop.index }}. {{ a.title }}.
            {% endfor %}

Three headlines, not five: at the median title length of 74 characters in our log, five titles is 370 characters read aloud, and nobody stands still for that. If the sensor is unavailable at 08:00 (two of our 150 polls got an HTTP 500), state_attr returns None and the template errors, so add a condition {{ state_attr('sensor.news_headlines', 'results') is not none }} before the action if the automation should fail quietly.

For a Voice Preview or any Assist satellite, replace tts.speak with assist_satellite.announce and the same message template.

Step 5: how often to poll, measured

This is the part the forum threads skip. One picked 20 minutes; the docs default is 30 seconds; neither number came from data. So we polled the exact query from Step 1 (ten results instead of five, to see more movement) once a minute for 150 minutes and logged the ten ids each time. The log is in data.csv next to this article.

The run: 150 polls between 07:12 and 09:41 UTC. 148 returned status: ok; two came back HTTP 500 one minute apart, which is what a rest sensor sees as a minute of unavailable. Across the 148 good polls 273 distinct headlines passed through the top 10: 1.79 new per minute on average, median 1, and 50 of 147 consecutive polls (34%) returned exactly the same ten. The newest headline on the card had been published a median of 5.1 minutes before we fetched it, so polling faster than every five minutes cannot make the card fresher than that. One caveat: this is one query on one Thursday morning (UTC). A single-category feed or a Sunday will turn over slower, so replay data.csv against your own query before trusting 15 minutes.

At a 1-minute interval 82% of the card is the same as the previous poll, at 5 minutes 32%, at 10 minutes 8%, and at 15 minutes or slower 0%

Replaying a slower scan_interval means taking every k-th poll and asking two things: what share of the card is identical to the previous poll (a wasted request), and how many of the 273 headlines the card would have shown at all.

scan_interval Requests/day Free tier (100/day) Pay-as-you-go, $0.01/req Card unchanged since previous poll Headlines shown in 2.5 h (of 273)
30 s (default) 2,880 exhausted in 50 min $864/mo not replayable from a 1-min log; above 82% —
1 min 1,440 exhausted in 100 min $432/mo 82% 273
5 min 288 no $86.40/mo 32% 207
10 min 144 no $43.20/mo 8% 139
15 min 96 yes $28.80/mo 0% 100
30 min 48 yes $14.40/mo 0% 50
60 min 24 yes $7.20/mo 0% 30

Requests per day are arithmetic (86,400 ÷ interval); the last two columns are replayed from data.csv. Plan limits and the $0.01 pay-as-you-go price are from the APITube pricing page on 17 September 2026; the free tier is 100 requests a day and 3,000 a month, at 10 requests a minute.

Below 15 minutes, requests go to re-fetching headlines the card already has: at 1 minute, 82 of every 100 rows are repeats. At 15 minutes and above nothing repeats, but the card samples a thinner slice of the day. The interval is not about freshness, because the API's own lag is about five minutes either way; it is a trade between request budget and how much of the news the card gets to show.

The rule we would give a friend:

  1. 15 minutes if the key is free: 96 requests a day fits under 100 with room for a manual reload, and in our run nothing on the card was a repeat.
  2. 60 minutes if the card is on a wall tablet nobody taps: 24 requests a day, the card still turned over completely between polls, it just showed 30 of the 273 headlines instead of 100.
  3. Never the default. 30 seconds is right for a temperature sensor and wrong for anything with a per-request price.

That is the whole build: one rest: block, one card, one automation, and a scan_interval with a reason behind it.

FAQ

How do I show news headlines on a Home Assistant dashboard? The simplest way to show news headlines on a Home Assistant dashboard is a rest: sensor with json_attributes: [results] pointed at a JSON news endpoint, rendered by a markdown card that loops over state_attr('sensor.news_headlines', 'results'). No custom card and no HACS component is required.

What is the difference between feedreader and a REST sensor in Home Assistant? The difference between feedreader and a REST sensor in Home Assistant is that feedreader polls hourly and exposes only the latest entry as an event entity, while a REST sensor polls on any scan_interval and stores the whole list of headlines as one attribute, which means a REST sensor can drive a multi-headline card on its own and feedreader cannot. Stacking feedreader events into an input_text runs into the same 255-character ceiling as a state.

How often does a Home Assistant REST sensor update? A Home Assistant REST sensor updates every 30 seconds by default (scan_interval: 30). For a news feed, 15 minutes (scan_interval: 900) is the shortest interval at which nothing on a 10-item card repeated between polls in our 150-poll measurement, and it uses 96 of a free key's 100 daily requests.

Why is my Home Assistant sensor state unknown after a template change? A Home Assistant sensor state becomes unknown when a template renders more than 255 characters, because MAX_LENGTH_STATE_STATE in core is 255. Keep the state short (a count or a timestamp) and put headlines in attributes, which have no length limit.

How do I make Home Assistant read the news out loud? To make Home Assistant read the news out loud, use an automation with a time trigger and a tts.speak action whose message is a Jinja template over the sensor's results attribute. Three headlines makes a short announcement; five, at a median 74 characters each, drags.

Resources