[Custom Integration] Oclean Toothbrush

Hi everyone,

I was looking for an Integration for the Oclean toothbrushes. I found some Posts (Oclean One bluetooth toothbrush) and Github Issus in the BLE Monitor Repository. Only the Battery Stats could be fetched this way.

I wondered how the app syncs that data, so i decompiled the official apk and let claude document all the bluetooth codes (and rest-api) to get all the needed Information for the HA Integration.
So I’ve been working on an unofficial Home Assistant integration for Oclean toothbrushes and I’m happy to share the first version with you!
Yes, it is nearly 100% generated by claude. I am a senior software developer with my main experience in java/js. I reviewed the code and tested the functionality.

What it does

Connects to your Oclean toothbrush every 5 minutes via Bluetooth, reads all brushing data, and disconnects. No cloud, no Oclean account required – fully local.

Sensors

  • :battery: Battery level
  • :star: Brush score (0–100)
  • :stopwatch: Brush duration
  • :clock1: Timestamp of last session
  • :tooth: Cleaned tooth zones (0–8) with per-zone pressure as attributes
  • :droplet: Average brushing pressure
  • :broom: Cleaning coverage (%)
  • :desktop_computer: Firmware version, model, hardware revision (diagnostic)
  • :toothbrush: Brush head usage counter + reset button

All brushing sessions are imported into HA long-term statistics with their actual timestamps – so historical sessions recorded while HA was offline show up correctly in your statistics graphs.

Installation

Add as custom repository in HACS:

and install the “Oclean Toothbrush (inofficial)” Integration in HACS

Or manually copy custom_components/oclean_ble/ to your config directory.

Setup

After restart: Settings → Integrations → Add → search “Oclean” The brush is auto-discovered if it’s nearby and Bluetooth is enabled.
Alternatively enter the MAC address manually.

Works with local Bluetooth adapters and ESPHome Bluetooth proxies.

Tested on

  • :white_check_mark: Oclean X – battery, score, duration, timestamp confirmed working

Other models (X Pro, X Pro Elite, X Ultra) should work but haven’t been tested yet. Two BLE protocol variants are implemented (Type-0 extended and simple format, Type-1 Oclean X format).

Looking for testers!

If you have an Oclean model other than the X, I’d love your feedback.

Enable debug logging, brush your teeth, and share the log output.

logger:
  logs:
    custom_components.oclean_ble: debug

Feedback, bug reports and PRs are very welcome. This is a community project – the protocol was reverse-engineered from the official Oclean APK.


Not affiliated with Oclean / Zhuhai Ice Bear Smart Home Technology Co., Ltd.

1 Like

Great Work!, off this back of this post I ordered 2 X Pro’s for my kids. While the first initiated first go, the second is throwing the below error:

I would say that the first one had no sessions saved where the second had been used once… not sure if this is part of the issue - importing of session data.

2026-02-27 18:20:31.206 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean time calibration sent (ts=1772176831)
2026-02-27 18:20:31.383 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean DIS model_id: OCLEANY3MH
2026-02-27 18:20:31.506 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean DIS hw_revision: HH

2026-02-27 18:20:31.608 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean DIS sw_version: 1.0.0.3
2026-02-27 18:20:31.805 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean subscribed to 5f78df94-798c-46f5-990a-855b673fbb86
2026-02-27 18:20:31.908 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean subscribed to 5f78df94-798c-46f5-990a-855b673fbb90
2026-02-27 18:20:31.908 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean could not subscribe to 6c290d2e-1c03-aca1-ab48-a9b908bae79e: Characteristic 6c290d2e-1c03-aca1-ab48-a9b908bae79e was not found! (BleakCharacteristicNotFoundError)
2026-02-27 18:20:31.908 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean could not subscribe to 5f78df94-798c-46f5-990a-855b673fbb89: esp32-bluetooth-proxy-109750 [E8:6B:EA:10:97:52]: Oclean X Pro - 70:28:45:83:1D:A1: Characteristic 5f78df94-798c-46f5-990a-855b673fbb89 does not have notify or indicate property set. (BleakError)
2026-02-27 18:20:32.011 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean notification raw: 0303020fd3600000
2026-02-27 18:20:32.011 DEBUG (MainThread) [custom_components.oclean_ble.parser] Oclean STATE parsed: {'battery': 96} (raw: 020fd3600000)
2026-02-27 18:20:32.012 DEBUG (MainThread) [custom_components.oclean_ble.parser] Oclean STATE unknown bytes – b0=0x02 (status? always 0x02 on OcleanX) b1=0x0f (unknown, varies) b2=0xd3 (unknown, varies) b4-5=0000 (unknown, always 0x0000 so far)
2026-02-27 18:20:32.012 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean notification parsed: {'battery': 96}
2026-02-27 18:20:32.012 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean CMD_QUERY_STATUS sent
2026-02-27 18:20:32.202 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean notification raw: 03080001021b120cdc5f733c4f4b
2026-02-27 18:20:32.203 DEBUG (MainThread) [custom_components.oclean_ble.parser] Oclean INFO response raw payload: 0001021b120cdc5f733c4f4b
2026-02-27 18:20:32.203 DEBUG (MainThread) [custom_components.oclean_ble.parser] Oclean INFO: could not parse payload, raw: 0001021b120cdc5f733c4f4b
2026-02-27 18:20:32.203 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean notification parsed: {}
2026-02-27 18:20:32.203 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean CMD_QUERY_RUNNING_DATA (0308) sent
2026-02-27 18:20:32.322 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean notification raw: 03072a422300001a021b120c0000007800780514
2026-02-27 18:20:32.322 DEBUG (MainThread) [custom_components.oclean_ble.parser] Oclean Type-1 INFO response raw: 2a422300001a021b120c0000007800780514
2026-02-27 18:20:32.323 DEBUG (MainThread) [custom_components.oclean_ble.parser] Oclean 0307 parsed: ts=1772176320 pNum=0 duration=120 s (raw: 2a422300001a021b120c0000007800780514)
2026-02-27 18:20:32.323 DEBUG (MainThread) [custom_components.oclean_ble.parser] Oclean 0307 extra bytes – validDuration=0x0078=120 s pressureArea[0]=0x05 pressureArea[1]=0x14
2026-02-27 18:20:32.323 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean notification parsed: {'last_brush_time': 1772176320, 'last_brush_pnum': 0, 'last_brush_duration': 120}
2026-02-27 18:20:32.324 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean CMD_QUERY_RUNNING_DATA_T1 (0307) sent
2026-02-27 18:20:32.424 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean CMD_QUERY_EXTENDED_DATA_T1 (0314) sent
2026-02-27 18:20:32.518 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean notification raw: 03090000004f4b
2026-02-27 18:20:32.519 DEBUG (MainThread) [custom_components.oclean_ble.parser] Oclean unknown notification type 0x0309, raw: 03090000004f4b
2026-02-27 18:20:32.519 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean notification parsed: {}
2026-02-27 18:20:34.522 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean no more sessions after page 0
2026-02-27 18:20:34.522 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean poll collected so far: {'model_id': 'OCLEANY3MH', 'hw_revision': 'HH\x00\n\x00\x04', 'sw_version': '1.0.0.3', 'battery': 96, 'last_brush_time': 1772176320, 'last_brush_pnum': 0, 'last_brush_duration': 120}
2026-02-27 18:20:34.659 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean battery raw: 60
2026-02-27 18:20:34.659 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean fetched 1 session(s) total from device (last_known_ts=0)
2026-02-27 18:20:34.659 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean  session[0]: ts=1772176320 (2026-02-27 18:12:00)  NEW
2026-02-27 18:20:34.691 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean importing 1 new session(s) into HA statistics:
2026-02-27 18:20:34.692 DEBUG (MainThread) [custom_components.oclean_ble.coordinator] Oclean 70:28:45:83:1D:A1 poll failed: cannot access local variable 'datetime' where it is not associated with a value (UnboundLocalError)
Traceback (most recent call last):
  File "/config/custom_components/oclean_ble/coordinator.py", line 245, in _async_update_data
    raw = await self._poll_device()
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/config/custom_components/oclean_ble/coordinator.py", line 409, in _poll_device
    await self._import_new_sessions(all_sessions)
  File "/config/custom_components/oclean_ble/coordinator.py", line 733, in _import_new_sessions
    datetime.datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S") if ts else "n/a",
    ^^^^^^^^
UnboundLocalError: cannot access local variable 'datetime' where it is not associated with a value

2026-02-27 18:20:34.692 WARNING (MainThread) [custom_components.oclean_ble] Oclean initial poll failed (Oclean device not reachable: cannot access local variable 'datetime' where it is not associated with a value) – integration will retry

Confirmed, I just used toothbrush 1 and now it’s exhibiting the same behaviour.

Unable to initiate connection X+ Pro after recording first brush session · Issue #8 · deniskie/ha-oclean-integration · GitHub opened