Skip to content

Repository files navigation

Toothbrush Card

hacs_badge GitHub Release License: MIT

A Custom Lovelace Card for Home Assistant designed to visualize electric toothbrushes via Bluetooth LE integrations.

Toothbrush Integrations

Brand Integration Status
Oral-B oralb (built into HA Core)
Oral-B oralb_live (custom component)
Philips Sonicare philips_sonicare_ble (custom component)
Xiaomi xiaomi_ble (built into HA Core)
Laifen laifen_ble (custom component)
Oclean oclean_ble (custom component) ⏸️
Playbrush ⏸️

✅ supported · ⏸️ not yet sensible

The readings the card can show depend on what each integration provides:

Reading Oral-B ⁵ Sonicare Xiaomi Laifen
Live timer ○ ¹
Sectors device-reported integration-derived ⁴ time-based ¹ time-based ²
Battery
Pressure ✓ ³
Intensity
Mode ✓ + select ✓ + select
Score
Brush head days · brushing hours left ⁷ wear · type · sessions wear
Ring color ✓ ⁶
Connection ✓ + ESP bridge

¹ the Xiaomi broadcast has no live timer/sectors — the card synthesizes a session timer with time-based quadrants
² aligned with the handle's 30s pacer when enabled; the routine length comes from Brushing Duration — the sensor on Wave Pro, the number entity on Wave (needs laifen_ble 3.0.3+)
³ pressure warning, Wave Pro only (needs laifen_ble 3.0.2+)
⁴ anatomical sectors provided by the integration's sector sensor, including revisit modes (e.g. White+); the card falls back to its own time-based calculation if the sensor is absent. Where the integration also publishes how the routine paces itself, the progress bar is divided into one segment per pacing step rather than per zone — a mode that revisits takes more steps than it has zones (Gum Health: ten over six), so a segment per zone would put every boundary in the wrong place
⁵ applies to both oralb and oralb_live — the latter mirrors the built-in integration's entities, so the card reads it identically. Its smiley sensor is shown in the done badge as the handle's own verdict on the finished session (icon reference); values that are not decoded yet display their raw name so they can be reported in #20. Its battery time remaining is not shown yet, same issue
oralb_live 0.7.22+ only — the color the handle's own light ring is set to; the card paints it as the accent when no accent color is configured (see Header)
oralb_live only — the handle counts a brush head down rather than up, in days and in brushing hours, and reports no lifetime total behind either, so the card shows the remainder itself instead of deriving a wear percentage from it (icon reference)

Tested devices

  • Oral-B: iO Series 6, iO 7/8/9 (Type 3758), iO 10, Pro Series D601
  • Sonicare: DiamondClean 9000 (HX992x), Prestige 9900 (HX999x), 7400 series (HX742x), HX960x, HX993x, Sonicare for Kids
  • Xiaomi: T700 · T500 uses the same integration and is expected to work — test reports welcome!
  • Laifen: Wave / Wave Pro

Not yet sensible

  • Oclean (oclean_ble) — rich per-zone statistics, but only after a session ends; no live brushing state the card could animate. Will revisit if a live status entity becomes available.
  • Playbrush — no Home Assistant integration available yet.

Want support for another brush? Open an issue — an integration with a live brushing state is the main requirement.

Screenshots

Oral-B Sonicare

The compact view (tooth_style: none) — shown here with the header and chips still enabled:

Compact

Wondering what a specific icon or colour means? The icon & colour reference shows every state icon the card can display, with its trigger condition and colour.

Supported Languages

Language Code Comment
English en
Dansk da
Deutsch de
Nederlands nl
Русский ru
Slovenščina sl

The card automatically detects the language configured in your Home Assistant instance (per-user profile setting). A specific language can also be forced per card with the language YAML option. If your language is not yet supported, it falls back to English. Contributions for additional languages are welcome — just add a new JSON file in src/locales/.


Features

  • Real-time brushing visualization with animated tooth SVG (4 or 6 sectors)
  • Chip-based display for battery, pressure/intensity, brushing mode, score, brush head and head time
  • Configurable layout: place every reading as a chip or a corner marker — or hide it
  • Compact panel mode: hideable header and tooth ring with a large standalone timer (two cards fit side by side on a 480×480 wall panel)
  • Bluetooth connection status indicator (Pantone 285 blue)
  • Sector-segmented progress bar based on the brushing target (uses the device routine length when available)
  • Session recap banner: green when the routine was completed, amber with partial progress when brushing stopped early — read from the brush's own record where an integration exposes one, otherwise rebuilt from recorder history, so it shows on any browser or device
  • A verdict on the finished session: the Oral-B display face or the Xiaomi score where the handle reports one — and on a handle that reports neither but records its own sessions, one the card works out from how far the session got and how much of it was brushed too hard (icon reference)
  • Automatic entity discovery — no manual YAML required
  • Sector tracking: device-reported (Oral-B), integration-derived (Sonicare) or time-based calculation (Xiaomi, Laifen)
  • Configurable title, subtitle, and accent color — left unset, the accent follows the color the brush's own light ring is set to, converted from the handle's LED drive levels so it matches what the ring shows
  • Configurable tooth, active-sector, and completed-sector colors
  • Configurable sector order with drag & drop and up/down buttons
  • Responsive layout with container queries (icon-only chips on narrow cards)
  • State-driven icons: battery, pressure, score and brush head step their icon shape and colour with the value, so they stay readable even icon-only — see the icon & colour reference
  • Multi-language support (auto-detects Home Assistant language)
  • Light and dark mode support via HA CSS variables

Community

Videos

Smartes Badezimmer? So hilft dir ein Shelly Wall Display beim Zähneputzen & Rasieren!

Smartes Badezimmer? So hilft dir ein Shelly Wall Display beim Zähneputzen & Rasieren! — Video by @smartmatic showing the card on a Shelly Wall Display alongside the Philips Shaver integration (German)

Blueprints

Toothbrushing Notification System (Morning/Evening) — a blueprint by @CoatsyJnr that reminds you to brush morning and evening, at deliberately unpredictable times so the reminder does not fade into background noise. It triggers on the same live brushing state this card visualizes, so the two go well together: the blueprint reminds, the card shows what is going on. On the Sonicare side, point it at the Brushing binary sensor — the other sensors its description suggests never fire, see CoatsyJnr/home-assistant-blueprints#1.

Installation

HACS (Recommended)

This card is available in the default HACS store — just click the button below, or search HACS for "Toothbrush Card".

Open in HACS

Then refresh your Home Assistant dashboard.

Manual installation
  1. Download dist/toothbrush-card.js from the latest release
  2. Copy it to /config/www/community/toothbrush-card/
  3. Add as a Lovelace resource:
resources:
  - url: /local/community/toothbrush-card/toothbrush-card.js
    type: module

Configuration

The card is configured via the UI — just add it and select your toothbrush device. The options below are grouped the same way as in the card editor.

Device

Option Type Default Description
device_id string (required) Toothbrush device to use

Header

Option Type Default Description
show_header boolean true false hides the whole card header (title, connection icons, menu) — useful for compact wall-panel dashboards
title string Custom title (default: manufacturer)
show_subtitle boolean true Show device name as subtitle
accent_color string Header accent color (hex, e.g. #0085FF). Left unset, the card follows the brush's own light-ring color where the device reports one (see below)

Some handles carry a light ring whose color can be set on the device, and some integrations read that color back — today that is oralb_live (0.7.22+) on Oral-B iO handles. Where a color is reported, an accent color left unset follows it: the card on the wall takes the color of the handle in the bathroom, and a household with several brushes tells them apart without configuring anything. Picking a color in the editor overrides it again; the Reset button next to the swatches clears the choice and hands the accent back to the brush. Where no color is reported — or while the sensor reads unknown or unavailable — the card stays with its own default.

What an Oral-B handle stores is not a screen color but the drive level of the three LEDs behind its ring, and they are not equally bright per unit of drive — which is why the handle's own "white" reads as #44CF63. The card converts before it paints, scaling each channel by what white says it is worth. The factors come from white alone, and they put the handle's yellow at hue 60° and its orange at hue 32°, exactly where those names belong; the values were read off two handles by stepping through the colour menu one name at a time. Values from an integration that reports an ordinary screen color are painted unchanged.

For integration authors: the ring color is picked up through its translation_key (ring_color) on the toothbrush device, and its state must be a six-digit hex color with the leading # (#0F5BCC). Anything else is ignored. The entity has to be enabled by default to be visible to the card at all; entity_category: diagnostic is fine. Report what the device stores — the card owns the conversion to a screen color, so a value corrected on the integration side would be corrected twice.

Visualization

Option Type Default Description
tooth_style teeth | none teeth none hides the tooth ring and shows a large standalone timer instead; the segmented progress bar still shows where you are
tooth_color string divider color Idle tooth color (hex). Set this if the teeth are invisible against your theme background
active_color string #93c5fd Color of the currently-brushing sector (hex)
done_color string #bbf7d0 Color of completed sectors (hex)
progress_size string slim Height of the progress bar: slim (4 px), bold (8 px) or xl (12 px) — the bigger sizes keep the bar readable from a distance on wall panels.
scale number 1.0 Scales the visual area — tooth ring, timer, status, corner markers and progress bar — between 0.8 and 2.0 for at-a-distance readability. Header and chips keep their size.

Sectors

Option Type Default Description
num_sectors 4 | 6 auto Override sector count. Auto-detected from the integration (Oral-B and Sonicare both expose it); manual override only needed as a fallback (e.g. unknown model or the diagnostic entity disabled)
sector_order string[] Custom sector order (drag & drop in UI)

Layout

Option Type Default Description
head_display string remaining What the brush-head reading shows: remaining (% left), used (% worn), or sessions (estimated sessions left — needs the integration's Sessions Left sensor, falls back to remaining without it).
layout object Place the readings (battery, pressure, intensity, mode, score, brush_head, head_time, head_type) freely: up to four chips in the top row (layout.chips) and four corner markers (layout.corners.top_leftbottom_right). Each reading can be used once; omitting layout keeps the classic arrangement. Editable in the UI.

Session recap

After a session ends the card keeps showing it as a banner: green "Brushing complete!" for a (nearly) full routine, amber "Brushing stopped early — x of y quadrants finished" for an aborted one. The × on the banner dismisses it in the current browser until the next session.

Where the recap comes from, in that order: the handle's own record of its last session, if the integration exposes one (a Last Session sensor) — it is the device's own account, it survives a Home Assistant restart, and it covers a session brushed while Home Assistant was off; otherwise a rebuild from recorder history, which works on any browser or device; otherwise the session the card watched end. Hovering the banner title says which one it was.

Option Type Default Description
hold_duration number 0.5 How long to keep showing the recap banner, in hours: 0.2524, or 0 = until the next brushing session starts.
hold_completed boolean true Whether the summary is shown at all: false turns the banner off, and with it everything below.
device_recap boolean true Use the handle's own record of the session, where the integration exposes one (a Last Session sensor). false ignores it and leaves the card with what it watched or can rebuild.
history_recap boolean true Rebuild the summary from recorder history when the card loads with nothing else to show (fresh browser or other device, sensor values already cleared by the brush). Uses one history query for the duration sensor; false disables it. Independent of device_recap: turning the reconstruction off does not refuse the handle's own account of itself.
show_verdict boolean true Show the verdict on the badge — the Oral-B display face (oralb_live), read from the session record where it carries one and otherwise from the live display, or the Xiaomi score, whichever the device reports. On a handle that reports neither but records its own sessions, the card works one out instead, from how much of its routine the session ran and — where the record carries it — how much of it was brushed too hard; hovering that face says so. The best of the four faces needs the pressure figure: without it a full session reads one step below, since "ran its course" and "ran its course and was brushed gently" are different sessions and only that reading tells them apart. false keeps the banner and drops only the glyph. It is one switch rather than a choice because no handle offers more than one of them.

For integration authors: a Last Session sensor is picked up through its translation_key (last_session). Its state is the time the session began — that is what handles stamp on their own records — with duration_seconds as an attribute or as a second entity (last_session_duration); the card works the ending out from the two. target_duration_seconds, session_id, superseded, time_source, display_face and source are used where present — source because a record read from the handle's own store (retained_session) and one the integration added up itself both arrive through this reading, and the badge says which of the two it is showing.

display_face is the verdict the handle's display showed when the session ended, in the same values as a live face reading. It is worth filing: the display sleeps about a minute later, so a dashboard opened after that — or one loaded after a restart — has no other way to the result. The card reads a missing attribute and an empty one differently, so please leave it empty rather than filling it in: absent means the integration does not report faces at all, and a face the card watched the display show still stands; empty means this session was recorded and no verdict was captured for it, and nothing is put in its place. A face that only arrives a moment after the record is picked up when it does.

Misc

Option Type Default Description
routine_length number auto Override the routine length in seconds (YAML only). Normally read from the device; mainly for brushes without one (e.g. Xiaomi, default 120) whose session timer is synthesized. Also the way out when a device has a routine-length reading but cannot fill it in: the card declines to judge a session against a guess, so the summary stays away until this is set.
language string auto Force the card language (YAML only): en, da, de, nl, ru or sl. Defaults to your Home Assistant profile language. Useful for shared wall panels or trying out a translation without changing your profile.

YAML Example

type: custom:toothbrush-card
device_id: 1234567890abcdef
title: My Toothbrush
show_subtitle: true
accent_color: "#0085FF"
tooth_color: "#d1d5db"
active_color: "#93c5fd"
done_color: "#bbf7d0"

Compact example (small wall panels)

Two of these fit side by side on a 480×480 panel (e.g. NSPanel Pro) — no header, no tooth ring, big timer, battery and mode flanking the status line:

type: custom:toothbrush-card
device_id: 1234567890abcdef
show_header: false
tooth_style: none
layout:
  chips: []
  corners:
    bottom_left: battery
    bottom_right: mode

A screenshot of the compact view is in the Screenshots section at the top.

Known Issues

  • 6-sector brushes (IO Series) on Home Assistant before 2026.8: Versions of the oralb_ble library up to 1.1.0 only map sectors 1–4, so 6-sector brushes see sectors 5/6 reported as sector 4 or as unknown_sector_code_N. This is fixed upstream (oralb-ble#180) and ships with Home Assistant 2026.8, which bundles oralb-ble 1.1.3 — there the card uses the reported sector directly. On older installations the card falls back to its built-in workaround, which tracks brushing progress and auto-advances past duplicate sectors.
  • iO Series 10 + iOsense charger: The iOsense smart charger pairs with the brush and holds its own Bluetooth connection to it whenever the charger is powered (that is how it shows real-time feedback on its LEDs). While that connection is active, the brush stops broadcasting the advertisements that the Home Assistant Oral-B integration listens for — so no entities update during brushing, regardless of how good your Bluetooth proxy or adapter is. Workaround: power the charger only when you actually want to charge (e.g. via a smart plug). See #3 for details.

Development

git clone https://github.com/mtheli/toothbrush-card.git
cd toothbrush-card
npm install
npm run build
npm test

npm test replays a real captured brushing session through the built card and checks what it renders, so it runs after npm run build. It needs no dependencies beyond Node itself — see test/.

Contributing a BLE capture for a protocol issue? See scripts/oralb/. Cutting a release? See docs/RELEASING.md.

Disclaimer

This is an independent community project and is not affiliated with, endorsed by, or sponsored by Philips or Oral-B. All product names, trademarks, and registered trademarks are property of their respective owners.

License

MIT License — see LICENSE

About

Custom Lovelace card for electric toothbrushes — supports Oral-B (oralb), Philips Sonicare (philips_sonicare_ble), Xiaomi T700 (xiaomi_ble) and Laifen (laifen_ble)

Topics

Resources

Stars

53 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages