Skip to main content
Selora Homes Selora Homes
Recipe · v1.0.0 · Released Aug 21, 2026

Oral-B: Kids' Brushing Check

Installs Home Assistant automations that record a brushing session from your Oral-B toothbrushes and notify you when the morning or evening brush never happened. Add a speaker and the household gets a spoken nudge before the deadline instead of a notification after it.

Asking whether the children brushed their teeth gets you an answer, not a fact. This recipe turns the toothbrush into the one that answers: it records a brushing session only when the brush has actually run for long enough to count, and if the morning or evening session never happened, it tells you at a deadline you choose.

It is built on the Oral-B integration in Home Assistant, which talks to the brush over Bluetooth and reports the length of the current session as a sensor. Home Assistant shows it as Duration on the toothbrush’s device page, sitting alongside the brushing state and the pressure reading. Because the recipe watches that counter rather than a simple on or off state, a child who pauses halfway through still gets credit for the full session, and a brush waved under the tap for a few seconds gets none.

How it works

During install you pick one toothbrush per person and set the two deadlines. The recipe then creates two flags per brush, one for the morning and one for the evening, and a small set of automations around them.

When a brush runs long enough to pass the minimum brushing time, the session is recorded against whichever window the clock is in: before the morning deadline it counts as the morning brush, from the start of the evening window it counts as the evening one. Sessions in the middle of the day count for neither, so a late morning brush cannot quietly tick the evening box. At each deadline, any brush whose flag is still off produces a push notification naming it. At midnight every flag clears and the next day starts fresh, and if Home Assistant happened to be restarting at midnight, the flags clear on the way back up instead.

Two minutes is the default minimum, which matches both the dentist’s advice and the toothbrush’s own timer, so children already know what it feels like. Lower it if a younger child never quite gets there and you would rather track the habit than the duration.

Nudging before the notification

A notification at the deadline tells you what already went wrong. The more useful half of this recipe is the reminder before it. Pick one or more speakers during install and the recipe speaks in the bathroom or hallway a few minutes ahead of each deadline, and only if a session is still missing, so a household that has already brushed is never nagged. Nobody is named out loud; the reminder is aimed at the room, and the notification is what names the brush.

The speaker selection is the switch: leave it empty and the recipe stays notification-only, with no spoken reminder in either window. The two reminder times only say when to speak, so set them a few minutes ahead of each deadline. To add or drop speakers later, reinstall the recipe and adjust the selection.

Before you install: Bluetooth coverage

Everything here depends on Home Assistant hearing the brush. Bathrooms are usually the far corner of the house from the server, so plan for a Bluetooth proxy within a few metres of the sink, an ESPHome Bluetooth proxy or any Home Assistant device that can act as one. Without it the brush reports nothing and every deadline looks like a missed brush.

Choose a proxy that supports active connections if you also want the battery level to stay current. Brushing time and state come through on passive advertisements, but the battery sensor needs a real connection to refresh.

The brushing timer on your dashboard

The community Toothbrush Card draws the session as a ring of teeth that fill in while a child brushes, which is what keeps them at the sink for the full two minutes. It supports Oral-B alongside Philips Sonicare, Xiaomi, and Laifen brushes.

You don’t need to install it. Choose a dashboard when the wizard asks and the recipe fetches the card, checks it against the version and digest it pins, registers it with Home Assistant, and places it pointed at the brush you picked. Refresh the browser once and the timer is there. Uninstalling the recipe removes the card and the file it downloaded.

If you already have the card from HACS, the recipe uses yours and downloads nothing. And if you would rather place it yourself, skip the dashboard step: the automations and notifications are unaffected, and you can add the card later from Edit dashboard then + Add card, or in YAML:

type: custom:toothbrush-card
device_id: 1234567890abcdef
title: Emma
tooth_style: teeth
progress_size: bold

The device_id is the toothbrush device in Home Assistant, which the card’s visual editor fills in for you when you pick the device from the list. In a household with several brushes the install places a card for the first one only, since the card shows a single device; add the others the same way.

Seeing the day’s status

Alongside the timer, an Entities card holding this recipe’s own flags makes the day readable at a glance:

type: entities
title: Teeth
entities:
  - input_boolean.brushed_smart_series_d700_morning
  - input_boolean.brushed_smart_series_d700_evening

The flag names follow your toothbrush entities, so a brush whose duration sensor is sensor.smart_series_d700_duration gets input_boolean.brushed_smart_series_d700_morning and ..._evening. Both appear in Home Assistant under Settings > Devices & services > Helpers after the install, where you can also flip one by hand on the day a child brushes at a friend’s house.

Files Selora AI's pipeline reads to install this recipe. The manifest declares roles, inputs, and integrations; the package files are Jinja-templated YAML applied to Home Assistant.

# Kids' Brushing Check: notice when a morning or evening brushing session
# never happened and say so, with an optional spoken nudge before the
# deadline.
#
# The Oral-B integration exposes a session-length sensor per toothbrush,
# counting the seconds of the current brushing session and resetting between
# sessions. It carries no name of its own upstream, so Home Assistant falls
# back to its device class and shows it as "Duration" on the device page.
# A session therefore shows up as that sensor climbing past a threshold,
# which is what this recipe triggers on. Counting seconds rather than
# watching the "Toothbrush state" sensor go running means a child who pauses
# halfway through still gets credit, and a brush switched on for five
# seconds does not.
#
# Per toothbrush the package creates two input_booleans, one for the morning
# window and one for the evening window. A qualifying session turns the flag
# for the current window on, a check at each deadline notifies about every
# flag still off, and a reset at midnight clears them for the next day.
slug: kids-brushing-check
version: 1.0.0
title: "Oral-B: Kids' Brushing Check"
tagline: Get a notification when a morning or evening brushing session never happened, with a spoken reminder before the deadline.
description: >-
  Watches the Oral-B toothbrushes you select and records a brushing session
  once the brush has run for long enough to count. If the morning or evening
  session has not happened by the deadline you set, it sends a push
  notification naming the brush. Optionally speaks a reminder on your
  speakers shortly before each deadline, so the household gets a nudge
  before anyone gets a notification.
author: Selora AI
released: "2026-08-21"
min_integration_version: "0.16.0"
tags: [bathroom, toothbrush, oral-b, kids, routines, alerts]
roles:
  # Session-length sensors: one per toothbrush. Multi-select so a household
  # with several brushes is covered by a single install, each brush tracked
  # separately. Two filters narrow the candidates:
  #   - ``integration: oralb`` keeps the Oral-B integration's own entities,
  #     so an appliance's remaining-time sensor can't be picked here.
  #   - ``device_class: duration`` picks the session-length sensor out of the
  #     ones the integration creates per brush. Matching on the device class
  #     rather than the name is what makes this resolve: the sensor has no
  #     name upstream, so it shows up as "Duration" rather than "Time", and
  #     it is the only Oral-B entity carrying that device class.
  # min_count 1: a brushing check with no toothbrush has nothing to watch.
  - id: brushing_timers
    title: Toothbrush session length
    kind: sensor
    integration: oralb
    device_class: duration
    min_count: 1
    selection: required
    description: >-
      The session-length sensor of each toothbrush you want tracked, shown as
      "Duration" on the toothbrush's device page in Home Assistant. Pick one
      per person.
  # Reminder speakers: optional. When at least one is picked, the recipe
  # speaks a nudge shortly before each deadline, but only if someone still
  # has not brushed. Leave empty and the recipe is notification-only.
  - id: announce_speakers
    title: Reminder speakers
    kind: media_player
    min_count: 0
    selection: required
    description: >-
      Optional. Speakers that say "time to brush your teeth" shortly before
      each deadline, and only when a brushing session is still missing.
      Leave empty to be notified on your phone only: this selection is what
      turns the spoken reminders on and off.
inputs:
  # How long the brush has to run before the session counts. Dentists ask
  # for two minutes, which is also the length of the brush's own timer, so
  # 120 seconds is both the recommendation and a value children recognise.
  # Feeds the numeric_state trigger's threshold.
  #
  # Bounded because a numeric_state trigger fires on the crossing, not on
  # the level: at 0 any flicker of the sensor books a session, and at a
  # negative value the sensor is already above the threshold and never
  # crosses it, so the check silently never fires. 10 seconds is the
  # floor at which "the brush ran" means anything at all.
  - id: min_brush_seconds
    type: number
    label: Minimum brushing time (seconds)
    description: >-
      How many seconds the brush must run before the session counts. Two
      minutes (120) matches the toothbrush's own timer.
    default: 120
    min: 10
    max: 600
  # Morning window. A qualifying session any time before the deadline counts
  # as the morning brush; the check fires at the deadline.
  - id: morning_reminder
    type: string
    label: Morning reminder time
    description: >-
      When to speak the morning nudge, if you picked speakers. Set a few
      minutes before the deadline.
    default: "07:45:00"
  - id: morning_deadline
    type: string
    label: Morning deadline
    description: >-
      The time the morning brush has to have happened by. A missing session
      is notified at this time.
    default: "09:00:00"
  # Evening window. Sessions between the morning deadline and this time
  # count for neither window, so a late morning brush cannot silently tick
  # the evening box.
  - id: evening_from
    type: string
    label: Evening window starts
    description: >-
      The time from which a brushing session counts as the evening brush.
      Sessions between the morning deadline and this time count for neither.
    default: "16:00:00"
  - id: evening_reminder
    type: string
    label: Evening reminder time
    description: >-
      When to speak the evening nudge, if you picked speakers.
    default: "19:45:00"
  - id: evening_deadline
    type: string
    label: Evening deadline
    description: >-
      The time the evening brush has to have happened by. A missing session
      is notified at this time.
    default: "21:00:00"
  # TTS engine entity for the spoken reminder. Auto-resolved by the
  # ``tts_engine`` resolver from the home's configured tts.* entities, so
  # the wizard never shows this field. ``tts.speak`` plus the resolved
  # engine works without a paid Home Assistant Cloud subscription. The
  # resolver returns an empty string when the home has no TTS engine, and
  # the template then skips the spoken reminder. The default below is only
  # used by the offline recipe-validation gate, which has no resolver to
  # run; at install the resolver always overwrites it.
  - id: tts_engine
    type: string
    label: Text-to-Speech engine
    description: >-
      Speech engine entity used for the spoken reminder. Resolved
      automatically from the Text-to-Speech engines configured in Home
      Assistant.
    resolver: tts_engine
    default: "tts.piper"
integrations:
  # Declared so a home without Oral-B set up gets told that, rather than
  # an empty picker: the roles are scoped to the oralb platform, so
  # without the integration the wizard has nothing to offer and the
  # reason is invisible. No auto_setup — Oral-B is a Bluetooth discovery
  # integration with no credentials and no form worth pre-filling; the
  # wizard's deep link lands on its page where HA's own discovery does
  # the rest.
  - domain: oralb
    title: Oral-B
package_files:
  - package/helpers/brushing_flags.yaml.j2
  - package/automations/session.yaml.j2
  - package/automations/reminders.yaml.j2
  - package/automations/checks.yaml.j2
  - package/automations/reset.yaml.j2
# The recipe's own output is a pair of flags per brush, but the thing a
# child will actually stand still for is the brushing timer, so the
# install drops the community toothbrush card onto a dashboard rather
# than leaving it as a copy-and-paste snippet in the docs.
#
# ``${device:brushing_timers}`` resolves to the device the first selected
# brush entity belongs to: the card targets a device rather than an
# entity, which is why it needs the device placeholder rather than
# ``${role:}``. In a household with several brushes only the first gets a
# card; add the rest by hand, one card per device.
#
# ``resource`` is what makes this self-contained: the install downloads
# the card, checks it against the digest below, serves it and registers
# it as a Lovelace resource. A home with no HACS, or with HACS but
# without this card, ends up with a working dashboard either way, and
# uninstalling takes the file and the registration back out.
#
# The URL is a pinned release asset, never a branch or "latest": a given
# recipe version installs the same bytes into every home, and moving to a
# newer card is a recipe change with a new digest, reviewed like any
# other. ``requires_resource`` still names the fragment to look for so a
# home that already has the card from HACS is left alone.
dashboard:
  card:
    type: custom:toothbrush-card
    device_id: ${device:brushing_timers}
    tooth_style: teeth
    progress_size: bold
    head_display: remaining
  resource:
    name: toothbrush-card
    url: https://github.com/mtheli/toothbrush-card/releases/download/v0.34.0/toothbrush-card.js
    version: v0.34.0
    sha256: 84b5cce8205aa28f38048f27925fb54b0f2aab08816862885aa2449cc8e8b951
  requires_resource: toothbrush-card
  target: default
  view: 0
{# Kids' Brushing Check, deadline checks: one automation per window.

   At the deadline the automation walks every toothbrush and sends one push
   notification per flag still off, naming the brush so a household with
   several knows which child to chase. Brushes that did record a session are
   passed over silently.

   repeat/for_each keeps this to a single automation per window whatever the
   number of brushes; the pairs are rendered at install time. The friendly
   name is read at runtime so renaming the device in Home Assistant is
   enough to change what the notification says, and the trailing " Duration"
   Home Assistant appends to it is trimmed off. #}
{# Helper and automation ids are derived from each brush's entity id with
   the sensor's suffix stripped, which keeps them readable. Two entities can
   strip to the same thing, though — ``sensor.alice`` and
   ``sensor.alice_duration`` both give ``alice`` — and that would collapse
   two brushes into one set of flags, silently leaving a child untracked.
   When any two collide, nobody gets the short name: the full object id is
   uglier but always unique. #}
{% set objs = [] %}
{% for brush in roles.brushing_timers %}
{% set _ = objs.append(brush.split('.')[1]) %}
{% endfor %}
{% set shorts = [] %}
{% for obj in objs %}
{% set _ = shorts.append(obj[:-9] if obj.endswith('_duration') else (obj[:-5] if obj.endswith('_time') else obj)) %}
{% endfor %}
{% set names = shorts if shorts | unique | list | length == shorts | length else objs %}
{% set windows = [
     {'id': 'morning', 'label': 'this morning', 'when': inputs.morning_deadline},
     {'id': 'evening', 'label': 'this evening', 'when': inputs.evening_deadline}
   ] %}
automation:
{% for w in windows %}
  - id: selora_recipe_{{ slug | replace('-', '_') }}_deadline_{{ w.id }}
    alias: "Brushing Check: {{ w.id }} deadline"
    description: >-
      At the {{ w.id }} deadline, notify about every toothbrush that has not
      recorded a brushing session.
    mode: single
    trigger:
      - platform: time
        at: "{{ w.when }}"
    action:
      - repeat:
          for_each:
{% for brush in roles.brushing_timers %}
{% set who = names[loop.index0] %}
            - brush: {{ brush }}
              flag: input_boolean.brushed_{{ who }}_{{ w.id }}
{% endfor %}
          sequence:
            - if:
                - condition: template
                  value_template: "{% raw %}{{ is_state(repeat.item.flag, 'off') }}{% endraw %}"
              then:
                - service: notify.notify
                  data:
                    title: "Teeth not brushed {{ w.label }}"
                    message: "{% raw %}{{ state_attr(repeat.item.brush, 'friendly_name') | default('A toothbrush', true) | replace(' Duration', '') }}{% endraw %} has not recorded a brushing session {{ w.label }}."
{% endfor %}
{# Kids' Brushing Check, spoken reminders: one automation per window.

   The nudge is deliberately the friendly half of the recipe. It fires
   before the deadline, only when at least one brushing session is still
   missing, and says the same thing to the room rather than naming and
   shaming whoever has not brushed. The notification at the deadline is what
   names the brush.

   Rendered only when the homeowner picked speakers and a TTS engine
   resolved, so a home that picked no speakers gets no reminder automation
   at all. The ``w.when`` guard is belt-and-braces: a blank input falls
   back to its default before it reaches here, so in practice a window
   always carries a time.

   The condition is a plain state condition per flag rather than a template,
   so it stays readable in the automation editor. #}
{# Helper and automation ids are derived from each brush's entity id with
   the sensor's suffix stripped, which keeps them readable. Two entities can
   strip to the same thing, though — ``sensor.alice`` and
   ``sensor.alice_duration`` both give ``alice`` — and that would collapse
   two brushes into one set of flags, silently leaving a child untracked.
   When any two collide, nobody gets the short name: the full object id is
   uglier but always unique. #}
{% set objs = [] %}
{% for brush in roles.brushing_timers %}
{% set _ = objs.append(brush.split('.')[1]) %}
{% endfor %}
{% set shorts = [] %}
{% for obj in objs %}
{% set _ = shorts.append(obj[:-9] if obj.endswith('_duration') else (obj[:-5] if obj.endswith('_time') else obj)) %}
{% endfor %}
{% set names = shorts if shorts | unique | list | length == shorts | length else objs %}
{% set windows = [
     {'id': 'morning', 'when': inputs.morning_reminder, 'say': 'Good morning. Time to brush your teeth.'},
     {'id': 'evening', 'when': inputs.evening_reminder, 'say': 'Time to brush your teeth before bed.'}
   ] %}
automation:
{% for w in windows %}
{% if roles.announce_speakers and inputs.tts_engine and w.when %}
  - id: selora_recipe_{{ slug | replace('-', '_') }}_reminder_{{ w.id }}
    alias: "Brushing Check: {{ w.id }} reminder"
    description: >-
      Shortly before the {{ w.id }} deadline, say it out loud if a brushing
      session is still missing.
    mode: single
    trigger:
      - platform: time
        at: "{{ w.when }}"
    condition:
      - condition: or
        conditions:
{% for brush in roles.brushing_timers %}
{% set who = names[loop.index0] %}
          - condition: state
            entity_id: input_boolean.brushed_{{ who }}_{{ w.id }}
            state: "off"
{% endfor %}
    action:
      - service: tts.speak
        target:
          entity_id: {{ inputs.tts_engine }}
        data:
          media_player_entity_id:
{% for player in roles.announce_speakers %}
            - {{ player }}
{% endfor %}
          message: "{{ w.say }}"
{% endif %}
{% endfor %}
{# Kids' Brushing Check, daily reset.

   Both windows clear at the turn of the day rather than each one clearing
   at its own deadline, so the flags keep reading as the record of the day
   just gone until the day is actually over. A late brush after the evening
   deadline still ticks its box; the notification has already been sent by
   then, so nothing fires twice.

   Midnight alone isn't enough: Home Assistant restarts, and an update
   that lands at 00:00 misses the trigger entirely. The flags are helpers,
   so they come back holding yesterday's answers, and the whole next day
   goes by with every check quietly passing. Firing on start as well
   covers that, guarded by the date of the last reset so a restart at
   breakfast can't wipe a brush that already counted this morning. #}
{# Helper and automation ids are derived from each brush's entity id with
   the sensor's suffix stripped, which keeps them readable. Two entities can
   strip to the same thing, though — ``sensor.alice`` and
   ``sensor.alice_duration`` both give ``alice`` — and that would collapse
   two brushes into one set of flags, silently leaving a child untracked.
   When any two collide, nobody gets the short name: the full object id is
   uglier but always unique. #}
{% set objs = [] %}
{% for brush in roles.brushing_timers %}
{% set _ = objs.append(brush.split('.')[1]) %}
{% endfor %}
{% set shorts = [] %}
{% for obj in objs %}
{% set _ = shorts.append(obj[:-9] if obj.endswith('_duration') else (obj[:-5] if obj.endswith('_time') else obj)) %}
{% endfor %}
{% set names = shorts if shorts | unique | list | length == shorts | length else objs %}
{% set reset_marker = 'input_datetime.' ~ slug | replace('-', '_') ~ '_last_reset' %}
automation:
  - id: selora_recipe_{{ slug | replace('-', '_') }}_reset
    alias: "Brushing Check: daily reset"
    description: >-
      At the turn of the day, and after any restart that missed it, clear
      every brushing flag so the new day starts from a clean slate.
    mode: single
    trigger:
      - platform: time
        at: "00:00:00"
      - platform: homeassistant
        event: start
    condition:
      # Skip when today's reset already happened. Home Assistant records
      # the date, not us: the helper survives the restart that makes this
      # trigger fire in the first place.
      - condition: template
        value_template: "{% raw %}{{ states('{% endraw %}{{ reset_marker }}{% raw %}') != now().date() | string }}{% endraw %}"
    action:
      - service: input_boolean.turn_off
        target:
          entity_id:
{% for brush in roles.brushing_timers %}
{% set who = names[loop.index0] %}
            - input_boolean.brushed_{{ who }}_morning
            - input_boolean.brushed_{{ who }}_evening
{% endfor %}
      - service: input_datetime.set_datetime
        target:
          entity_id: {{ reset_marker }}
        data:
          date: "{% raw %}{{ now().date() | string }}{% endraw %}"
{# Kids' Brushing Check, session recorder: one automation per toothbrush.

   The Oral-B integration's "Time" sensor counts the seconds of the current
   brushing session and resets between sessions, so a real brush shows up as
   that sensor climbing past the threshold. A numeric_state trigger fires on
   the crossing only, which means a child who pauses mid-session still gets
   credit once the brush has run for long enough in total, and a brush
   switched on for a few seconds never does.

   The condition guards the one false positive this shape has: a Bluetooth
   reconnect can take the sensor straight from unavailable to a full count,
   which the trigger would otherwise read as a session. Requiring the
   previous state to have been a real number drops those.

   Which window the session counts for is decided by the time of day.
   Sessions between the morning deadline and the start of the evening window
   count for neither, so a late morning brush cannot silently tick the
   evening box. #}
{# Helper and automation ids are derived from each brush's entity id with
   the sensor's suffix stripped, which keeps them readable. Two entities can
   strip to the same thing, though — ``sensor.alice`` and
   ``sensor.alice_duration`` both give ``alice`` — and that would collapse
   two brushes into one set of flags, silently leaving a child untracked.
   When any two collide, nobody gets the short name: the full object id is
   uglier but always unique. #}
{% set objs = [] %}
{% for brush in roles.brushing_timers %}
{% set _ = objs.append(brush.split('.')[1]) %}
{% endfor %}
{% set shorts = [] %}
{% for obj in objs %}
{% set _ = shorts.append(obj[:-9] if obj.endswith('_duration') else (obj[:-5] if obj.endswith('_time') else obj)) %}
{% endfor %}
{% set names = shorts if shorts | unique | list | length == shorts | length else objs %}
automation:
{% for brush in roles.brushing_timers %}
{% set who = names[loop.index0] %}
  - id: selora_recipe_{{ slug | replace('-', '_') }}_session_{{ who }}
    alias: "Brushing Check: session recorded ({{ who | replace('_', ' ') | title }})"
    description: >-
      When this toothbrush has run long enough to count as a brushing
      session, mark the current window as done.
    mode: single
    trigger:
      # numeric_state's ``above`` is strictly greater than, so passing the
      # minimum straight in would drop the session that stops exactly on it,
      # which is the likely one: the brush's own two minute timer against a
      # 120 second threshold. Half a second under makes the comparison
      # inclusive, and is far too small a margin to let a session that
      # genuinely fell short slip through.
      - platform: numeric_state
        entity_id: {{ brush }}
        above: {{ inputs.min_brush_seconds - 0.5 }}
    condition:
      - condition: template
        value_template: "{% raw %}{{ trigger.from_state is not none and trigger.from_state.state not in ['unknown', 'unavailable'] }}{% endraw %}"
    action:
      - choose:
          # Before the morning deadline: this is the morning brush.
          - conditions:
              - condition: time
                before: "{{ inputs.morning_deadline }}"
            sequence:
              - service: input_boolean.turn_on
                target:
                  entity_id: input_boolean.brushed_{{ who }}_morning
          # From the start of the evening window: this is the evening brush.
          - conditions:
              - condition: time
                after: "{{ inputs.evening_from }}"
            sequence:
              - service: input_boolean.turn_on
                target:
                  entity_id: input_boolean.brushed_{{ who }}_evening
{% endfor %}
{# Kids' Brushing Check helpers: two flags per toothbrush, one per window.

   The flag is the recipe's memory of the day. A qualifying session turns
   the flag for the current window on, the check at each deadline notifies
   about every flag still off, and the midnight reset clears them all.

   The object id is derived from the brushing time sensor's own entity id
   with the trailing ``_time`` removed, so a home with several brushes gets
   one stable pair of flags per brush that survives a reinstall. #}
{# Helper and automation ids are derived from each brush's entity id with
   the sensor's suffix stripped, which keeps them readable. Two entities can
   strip to the same thing, though — ``sensor.alice`` and
   ``sensor.alice_duration`` both give ``alice`` — and that would collapse
   two brushes into one set of flags, silently leaving a child untracked.
   When any two collide, nobody gets the short name: the full object id is
   uglier but always unique. #}
{% set objs = [] %}
{% for brush in roles.brushing_timers %}
{% set _ = objs.append(brush.split('.')[1]) %}
{% endfor %}
{% set shorts = [] %}
{% for obj in objs %}
{% set _ = shorts.append(obj[:-9] if obj.endswith('_duration') else (obj[:-5] if obj.endswith('_time') else obj)) %}
{% endfor %}
{% set names = shorts if shorts | unique | list | length == shorts | length else objs %}
input_boolean:
{% for brush in roles.brushing_timers %}
{% set who = names[loop.index0] %}
  brushed_{{ who }}_morning:
    name: "{{ who | replace('_', ' ') | title }}: brushed this morning"
    icon: mdi:tooth-outline
  brushed_{{ who }}_evening:
    name: "{{ who | replace('_', ' ') | title }}: brushed this evening"
    icon: mdi:tooth-outline
{% endfor %}
{# The date the flags were last cleared. Read by the reset automation so a
   restart that missed midnight still starts the day clean, while a restart
   at breakfast leaves a brush that already counted alone. #}
input_datetime:
  {{ slug | replace('-', '_') }}_last_reset:
    name: "Brushing check: last reset"
    has_date: true
    has_time: false
    icon: mdi:calendar-check

Author-maintained release notes. Each release of the recipe lists what changed.

Changelog

v1.0.0 - 2026-08-21

Initial release.

  • Records a brushing session from each Oral-B toothbrush you select, reading the session-length sensor Home Assistant shows as Duration on the toothbrush’s device page, and counting a session only once the brush has run for the minimum brushing time, so a pause mid-session still counts and a five second brush does not.
  • Tracks a morning and an evening window per toothbrush, with the deadlines and the start of the evening window set during install.
  • Sends a push notification at each deadline naming every toothbrush that has not recorded a session.
  • Optionally speaks a reminder on one or more speakers shortly before each deadline, and only when a session is still missing; leave the speaker selection empty to stay notification-only.
  • Clears every flag at the turn of the day so each day starts from a clean slate, and again after a restart that missed midnight, so an overnight Home Assistant update can’t leave yesterday’s answers standing all day.
  • Installs the community Toothbrush Card and places it on a dashboard, pointed at the brush you picked. The card is downloaded from a pinned release and checked against a digest, so no HACS install is needed; a home that already has the card keeps its own copy. Uninstalling removes both the card and the downloaded file.
  • Ignores the jump from unavailable to a full brushing count that a Bluetooth reconnect produces, so a reconnect is never mistaken for a session.

Type to search across cities, counties, and installers

↑↓ navigate open
⌘K search