A native ESPHome external component for Philips BLE shavers (Series 5000–9000,
OneBlade, i9000 — anything exposing the proprietary 0x0100/0x0300 GATT
services). Runs entirely on an ESP32: pairing, decoding and control happen
locally, with no cloud, no phone app, and no Home Assistant dependency at
runtime.
Developed and hardware-tested against an S7882; the protocol layer follows
the reverse-engineered reference in docs/PROTOCOL.md
(decompiled official APK + upstream community integration + live captures),
so other models should degrade gracefully: characteristics a model does not
expose simply leave their entities empty.
Current version: 1.16.0 — 70 GATT data points decoded. See
CHANGELOG.md.
- Full sensor coverage: battery, motor RPM/current (raw ÷ 3.036), pressure (signed, as the manufacturer app reads it), head wear, session stats, charge/cleaning cycles, per-session motion summary, sanitizing case, and more — plus diagnostic text sensors (mode, motion, load type, notifications, firmware/serial…).
- Control: shaving mode select, light-ring brightness, GuidanceModes
coaching select (the two-bit field at
0x0319bits 4–5, written correctly as a unit), light-ring RGB colours with a working anti-echo, charging-stand animation, greeting, blade-replacement reset, notification management. - History playback: the on-device session log (≈20 sessions) is replayed
as
esphome.philips_shaver_historyHome Assistant events, in the exact read-record-then-advance sequence the device expects. - Cleaning cartridge model: usage-based estimate persisted in NVS, with an SNTP clock or banked-uptime fallback.
- Robust BLE: LE Secure Connections pairing with auto-confirmed Numeric
Comparison, a single-ATT-slot state machine with watchdogs on reads,
writes and subscriptions, reconnect backoff, stale-bond purge, and decoded
SMP failure reasons (
PAIR_NOT_SUPPORTmeans the shaver's single pairing slot is taken — see Troubleshooting). - Research tools:
read_characteristic/write_characteristicservices, a GATT table dump, a BLE scan helper, and anexplore_unknownmode that watches the characteristics nobody has named yet.
- ESP32 with the esp-idf framework (Bluedroid).
esp32_ble_trackerwithactive: true— the shaver only advertises its service UUID in the scan response.esp32_ble: io_capability: display_yes_noandmax_notifications: 30(the table declares 27 notifying characteristics — 24 features plus 3 explorer targets — so a maxed-out model subscribes up to 27; an S7882 lands at 19 with the explorer on. Bluedroid's default of 5 is far too low).CONFIG_BT_GATTC_MAX_CACHE_CHAR: "100"— the shaver exposes ~66 characteristics, the default cache of 40 truncates discovery.api: custom_services: trueandhomeassistant_services: true(the component registers services and fires events unconditionally).- Optional but recommended: an SNTP
time:platform for the cartridge model and thesync_time_of_daybutton.
Copy components/philips_shaver_native/ next to your YAML and reference it:
external_components:
- source:
type: local
path: components
components: [philips_shaver_native]
ble_client:
- mac_address: "AA:BB:CC:DD:EE:FF" # static random — find it with scan_ble
id: shaver_ble
auto_connect: true
philips_shaver_native:
id: shaver
ble_client_id: shaver_ble
battery_level:
name: "Battery"
pressure:
name: "Pressure"
connected:
name: "Connected"The full example shows every entity, the
recommended radio/boot ordering (enable_on_boot: false + ble.enable after
Wi-Fi), and the two options the colour lights require
(gamma_correct: 1.0, default_transition_length: 0s — without them the
anti-echo cannot match bytes and colour changes bounce back over BLE).
The shaver holds one pairing slot. It must be unpaired from the phone,
the app and on the handle before the ESP can bond. Pairing is LE Secure
Connections; the component auto-confirms the Numeric Comparison. If logs show
PAIR_NOT_SUPPORT, the slot is taken — clear it on the shaver itself, then
press the Pair button entity. Full flow and failure codes:
docs/TROUBLESHOOTING.md.
| Document | Contents |
|---|---|
docs/CONFIGURATION.md |
Every option, entity, service and event, with the GATT source of each value |
docs/PROTOCOL.md |
The BLE protocol reference: services, characteristics, enums, composite blocks, write payloads, open questions |
docs/TROUBLESHOOTING.md |
Pairing, failure codes, backoff, discovery timing, empty entities |
docs/ARCHITECTURE.md |
How the component works inside: phase machine, watchdogs, anti-echo, history flow, explorer |
CHANGELOG.md |
Version history, including which decodes are speculative |
Honesty section. These are documented in code comments and the changelog too:
- An S7882 emits 10, 13, 19, 28 on the motion characteristic
0x0305— values the official app cannot decode either. Themotion_rawsensor and the three per-session motion sensors exist to chart this. - The sanitizing-case field sizes, the
0x0315record packing and the0x0329time encoding are flagged assumptions — decoded to entities or logs with the raw bytes available for verification. - The
0x0600BLE Serial Service is named by the APK but its payload format is undocumented: out of scope until someone captures it.
Logs from other models — especially explore lines and Resolved N/70
counts — are the most useful contribution.
Protocol knowledge assembled from the decompiled official Philips APK, the
upstream community integration this component mirrors where it is right (and
diverges from where it is not — see docs/PROTOCOL.md §10), live BLE
captures on an S7882, and Philips patent
US11154997B2 for the
motion-histogram model.