Tuya Wifi Module replacements in a cat feeder

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.