tl;dr just go to the repo
Having recently been acquired by the Cat Distribution System, I decided it was about time I get a pet feeder connected to Home Assistant. I did some research online, found Yet Another Tuya based Pet/Cat Feeder in ESPHome posted here, and that inspired me. To take a shortcut.
I don't want to desolder the wifi module, and fortunately, with this specific model, I didn't end up needing to. Instead, I disabled the WBR3 wifi module, and put an ESP32-S3 Super Mini in its place, speaking exclusively to Home Assistant.
This approach opens up other opportunities to take control over a lot of the Tuya hardware that's out there without having to completely rewrite the firmware.
Hardware Overview
This is a two-chip TuyaMCU design:
| Component | Role |
|---|---|
| Puya PY32F003 (ARM Cortex-M0+) | Application MCU — motor, schedule, sensors |
| Tuya WBR3 (RTL8720CF, Wi-Fi+BT) | Wi-Fi radio only |
The PY32 and WBR3 communicate over 9600-baud UART using the Tuya serial protocol. You don't control this feeder via GPIO — you speak TuyaMCU using the tuya component. The actual hardware of the feeder itself is somewhat irrelevant, because it's all still controlled via the MCU firmware. The only thing we have to do is issue instructions and listen to events.
Key fact: the WBR3 sits on the opposite side of the PCB from the PY32 and motor circuitry. It's silenced by pulling its EN pin to GND — reversible, no desoldering required.
Wiring
The ESP32-S3 takes over the WBR3's side of the UART.
| Connection | Detail |
|---|---|
| Disable WBR3 | Jumper WBR3 EN pad → GND |
| ESP32-S3 RX (GPIO4) | Tap PY32 TX line (WBR3 comm-UART pad) |
| ESP32-S3 TX (GPIO5) | Drive PY32 RX line (WBR3 comm-UART pad) |
| Baud | 9600, 8N1 |
| ESP power | Tap feeder's USB-C 5V rail |
| Logger | USB_SERIAL_JTAG (onboard USB, no GPIO collision) |
Confirmed during bring-up: the PY32 transmits on its TX line regardless of WBR3 EN state. Pulling EN low cleanly silences the radio without stopping device logic.
GPIO4 and GPIO5 are both clean on the S3 — non-strapping, non-USB, non-flash territory.
Web UI
Because the schedule is something I don't modify frequently, I decided that the full UI/UX for managing the schedule could be served by the ESP itself as a web app. To do this, I needed to flash a javascript and header file alongside the yaml configuration. By including those files, I now have a fully custom UI rendered at the device's address on my LAN.
Datapoint Map
| DP | Code | Access | Type | Values |
|---|---|---|---|---|
| 1 | meal_plan |
rw | raw (≤128 B) | Up to 10 meals × 5 bytes — see schedule encoding |
| 3 | manual_feed |
rw | value | 1–12 portions |
| 4 | feed_state |
ro | enum | 0=standby, 1=feeding, 2=done |
| 6 | status |
ro | enum | 0=enough, 1=insufficient, 2=run_out |
| 10 | battery_percentage |
ro | value | 0–100% |
| 11 | charge_state |
ro | bool | charging |
| 13 | fault |
ro | bitmap | bit0=jam, bit1=food_low, bit2=run_out, bit3=desiccant, bit4=battery_low |
| 14 | feed_report |
ro | value | 0–12 portions dispensed |
| 24 | factory_reset |
rw | bool | — |
Schedule Encoding (dp1)
Each meal is 5 bytes, concatenated:
[weekday_bitmap][hour][minute][portions][enable]
| Field | Format |
|---|---|
| Weekday bitmap | Mon=0x40, Tue=0x20, Wed=0x10, Thu=0x08, Fri=0x04, Sat=0x02, Sun=0x01. All days=0x7F, weekdays=0x7C, weekend=0x03 |
| Hour | Plain byte (0–23) — not BCD |
| Minute | Plain byte (0–59) — not BCD |
| Portions | Plain byte (1–12) |
| Enable | 0x00=off, 0x01=on |
Worked example: Sunday 18:50, 12 portions, enabled → 01 12 32 0C 01
No-schedule sentinel: FF FF FF (fresh device default)
Calling the Schedule Service
I haven't actually had to do this, since I just manage the schedule from the device's IP directly. If you want to set the schedule via an action, it's also possible. In Developer Tools → Actions:
action: esphome.pet_feeder_set_meal_plan
data:
# Weekday bitmaps: Mon-Fri=124 (0x7C), Every day=127 (0x7F), Weekend=3 (0x03)
days: [127, 127]
hours: [8, 18]
minutes: [0, 0]
portions: [3, 4]
enabled: [true, true]
This writes a schedule for every day at 08:00 (3 portions) and 18:00 (4 portions).
The MCU echoes the dp1 bytes back on a 0x07 report — confirm success in ESPHome logs by looking for Datapoint 1 update to ... with your encoded values.
Important Details
force_set for manual feed
The MCU does not auto-reset dp3 to 0 after feeding. ESPHome's normal set_integer suppresses writes when the value hasn't changed. Repeated feeds at the same portion count silently fail. Use force_set_integer_datapoint_value() (as shown in the feed_now action) to always emit the command.
Feed report latching
dp14 (feed_report) is a transient pulse — the MCU reports portions fed, then resets to 0 after ~2 seconds. If you build a sensor on dp14, add a filter:
filters:
- lambda: |-
if (x == 0) return {}; # drop the MCU's post-feed reset
return x;
The 0x0C GMT time query
Per Tuya's "Time Service" MCU Standard Protocol, the device polls for time in two ways: 0x1C for local time (already handled by ESPHome's tuya: component) and 0x0C for GMT (no handler exists). The MCU sends 0x0C (~every 5s) with an empty payload. The module's GMT response is a 7-byte payload {valid, year-2000, month, day, hour, minute, second} — same as local time but without the trailing weekday byte.
I've submitted a pull request to support this poll request, but until it's merged, I've just pointed esphome to use the tuya component from my repo instead.
init_state 0 at boot
The log line init_state 0 during boot is expected — it's logged before the handshake completes. Judge success by the later DP dump when init_state reaches 5.
What Runs Now
- Local LAN control via ESPHome native API — no cloud, no Tuya dependency
- Live battery and food-level sensors in Home Assistant
- Real-time feed state and fault detection
- Manual feed trigger (via button or automation)
- Offline schedule execution — the MCU runs the meal plan independently from its own battery, even during total network outage
The full config is in a github repository that you can clone and use as a reference. I'll be doing my best to add other tuya-based configurations for other hardware that I've purchased as I build it.



