I bought a new cat litter box at Action for €129,- yesterday.
I spent an afternoon with my mate Claude Code getting a Hombli Petmo C1 automatic litter box (model PMLB-0109) working fully locally in Home Assistant. It isn’t supported by any existing integration and several of its data points are mislabelled or dead, so I’m writing down everything I found in case it saves someone else the same afternoon.
Short version: it works completely locally, but not with the LocalTuya most people have installed, and about a third of the data points don’t do what their names suggest.
1. It speaks Tuya protocol 3.5
This is the first wall you’ll hit. The widely installed rospogrigio/localtuya only supports up to protocol 3.4 and fails with empty_dps, which looks like a wrong local key but isn’t.
Use xZetsubou/hass-localtuya instead — the maintained fork, same localtuya domain, supports 3.5. It migrates the existing config entry: I had another Tuya device in the same entry and it came through untouched. Take a backup first anyway.
Determine the protocol version with tinytuya rather than guessing in the config flow:
import tinytuya
d = tinytuya.Device(DEVICE_ID, IP, LOCAL_KEY, version=3.5)
print(d.status())
Wrong version gives a distinctive error: Check device key or version for 3.4, Unexpected Payload for 3.1–3.3. Five seconds of testing beats twenty minutes of config-flow roulette.
A Tuya device accepts only one local connection at a time. Stop your diagnostic script before letting LocalTuya take over, or they’ll fight over the socket.
2. Auto-configure only works if you set up the Cloud API first
In the fork’s options flow, auto_configure_device silently bounces back to the menu if you haven’t configured Cloud API credentials. Without them it only sees bare DP numbers, not their codes, so it can’t map platforms.
Configure Cloud API (region, client id, client secret, user id) first, then auto-configure works in one go — and as a bonus it fills in proper model names for your other Tuya devices.
Note that auto-configure only creates the standard entities for the category (5, in my case). Everything else you add by hand via Edit device → Add entity.
3. The complete DP map
Pulled from the Tuya Cloud API and then verified one by one against the physical device.
Tip: /v2.0/cloud/thing/{device_id}/shadow/properties returns dp_id + code + value for all data points in one call. That’s far more useful than /v1.0/iot-03/devices/{device_id}/specification, which only returns the category’s standard instruction set. And it doesn’t touch the local connection.
| DP | Code | Type | What it actually is |
|---|---|---|---|
| 6 | cat_weight |
int | Cat weight in grams (spec says scale 3) |
| 7 | excretion_times_day |
int | DEAD — always 0, never counts |
| 8 | excretion_time_day |
int | Duration per visit in seconds (not per day, despite the name) |
| 17 | deodorization |
bool | DEAD — never reports, not the ioniser |
| 22 | fault |
bitmap | bit0 motor, bit1 program, bit2 g-sensor |
| 101 | Clean |
bool | Start cleaning and a usable status: true while running, device clears it when done |
| 102 | empty |
bool | Empty out — dumps everything including litter |
| 103 | trash_status |
bool | DEAD — never reports, despite a FULL lamp on the panel |
| 104 | monitoring |
bool | Cat present — only works if DP 111 is false |
| 105 | Induction_Clean |
bool | Auto-clean setting. Dips to false during a cycle, restores itself |
| 111 | Clean_notice |
bool | |
| 114 | child_lock |
bool | Write-only — see below |
| 117 | induction_delay |
int | Clumping time, minutes |
| 118 | induction_interval |
int | Cleaning interval, minutes |
| 120 | odourless |
bool | The ioniser (not DP 17). Auto-expires after a while |
| 121 | smart_clean |
bool | Unused here |
| 123 | usage_times |
int | Stuck at 1, does not increment on a visit |
| 124 | capacity_calibration |
int | Calibration value |
| 125 | sand_surface_calibration |
int | Calibration value |
| 126 | auto_deordrizer |
bool | Auto-ioniser: runs DP 120 after every clean cycle |
| 127 | detection_sensitivity |
int | Grams |
| 128 | number |
int | Unknown, never moves |
| 129 | ibs |
int | Weight in lbs × 100 — handy cross-check on DP 6 |
| 130 | deport_mode |
bool | Unused here |
| 131 | weight_correction |
int | Load-cell calibration factor — not a fill level |
| 132 | reset |
bool | |
| 133 | restart |
bool |
4. The traps, in order of how much time they cost me
DP 111 is the IR sensor setting, with inverted logic. The cloud calls it Clean_notice, which is meaningless. In the app it’s “Disable the infrared sensor”. true = IR sensors off = the box does not stop when it detects movement during a cycle. Mine shipped with this enabled. With two kittens that’s worth checking today.
It also silently breaks your statistics: with IR off, DP 104 never reports, so you have no cat-presence signal at all.
DP 7 doesn’t count, so you have to count visits yourself. Detect them on DP 104 going on → off. Don’t rely on DP 6 or DP 8 changing value: if two cats weigh the same and take the same time, nothing changes and you miss the visit entirely. I learned this the hard way — three visits, one counted.
DP 6 and DP 8 do not arrive at the same moment. I measured 115 ms between them. If your automation reads both at trigger time, you’ll record the previous visit’s duration. Add a short delay and read the values afterwards — and in HA that means a mid-sequence variables: step, because a top-level variables: block renders when the run starts, not after your delay.
The weight scaling lives in the LocalTuya entity, so fix it there. The device sends grams (1800). With the entity’s correction factor at 0.1 you get 180 labelled “kg”. Set the factor to 1 and the unit to grams. Note that this factor also determines the raw_state attribute, so don’t try to compensate for it in a template sensor — fix it at the source.
DP 114 (child lock) is write-only. It never reports its state, not on push and not even when explicitly requested with localtuya.update_dps. Worse: switch.turn_on worked but switch.turn_off did nothing, twice — I suspect because the entity has no known baseline state. localtuya.set_dp works in both directions. I ended up driving it from an input_boolean with an automation calling set_dp, plus a homeassistant.start trigger to re-sync after a restart. Optimistic state, but this box’s app is the only other way to change it.
DP 101 looks stuck if you measure too early. It stays true for the whole cycle, which is several minutes. I nearly concluded it was a latching command; it clears itself the moment the cycle ends and is genuinely useful as a “cleaning in progress” indicator.
DP 105 dips during a cycle. Twice I thought something had switched my auto-clean off. It goes false while cleaning and restores itself afterwards.
5. What I built on top
Because DP 7 and DP 103 are dead, HA does the bookkeeping:
- Visit counting from DP 104, with DP 6/DP 8 changes as a fallback and a 60-second dedup window (several triggers fire for one visit)
- Waste drawer level estimated from visits since the last emptying, with a button to press when you actually empty it. It records how many visits that cycle lasted and keeps the longest cycle seen — that’s a better capacity estimate than the last one, because you sometimes empty early and that tells you nothing
- Per-cat attribution by weight, with a deliberate dead zone: if a visit falls within a margin of the threshold it’s recorded as “unassigned” rather than guessed wrong
- Notifications with two independent layers: per person and per alert type
6. Things I still don’t know
- What
usage_times(123) andnumber(128) are for - Whether DP 103 ever fires — the panel has a FULL lamp, so the hardware detection exists; it just isn’t pushed locally
- Whether the app’s schedules (DP 106/107/110/122) can be driven locally — those DPs exist in the cloud but my unit never pushes them
Happy to answer questions. If you have the same box or a rebadged sibling (these OEM chassis show up under a lot of brand names), I’d be curious whether your DP 7 and DP 103 are as dead as mine.


