Introduction: ESPHome / Home Assistant Cat Feeder

I bought an automatic pet feeder from Amazon Link to Amazon.

Model Number: spf-1010-ty.

it's a generic feeder that dispenses about half an egg-cup of food per "portion".

It uses Tuya as the internet connection method, so I decided to convert it to ESPHome and run some custom firmware to remove Tuya, and connect it to Home Assistant.

The pet feeder now runs without cloud connection. By connecting it with Home Assistant, I can command Alexa or Google Home to dispense food, or set up a schedule to automaticly feed the pet.

You'll need ESPHome and Home Assistant installed for this project, and assumes that you have experience with both applications.

For nerds, here's a link to the ESP device on the PCB:

https://developer.tuya.com/en/docs/iot/wifie3smodu...

Supplies

Step 1: Dissasembly

Remove the small screw and pull off the stirring wand.

Turn over the feeder, and remove the 3 screws.

Remove the 3 screws holding down the PCB, remove PCB.

Wiggle and pull out the cables from the PCB.

Step 2: Connecting the USB to Serial Converter

Any USB to Serial converter can be used. It's helpful if the device has a 3 volt output as this can be used to power the board during programming.

!! MAKE SURE THE POWER SUPPLY IS 3 VOLTS !!

  • Connect 4 wires as shown to the PCB and then to a Serial Converter board:
  • Connect the TX on the device (see photo) to RX on the Serial converter.
  • Connect the RX on the device (see photo) to TX on the Serial converter.
  • Connect 3 Volts to the PCB, (from the Serial Converter or an external power source), but don't turn on power yet.
  • Connect Gnd to the Serial Converter and the PCB

Step 3: Compiling the Code

Within ESPHome, create new project. I called my project "spf-1010-ty-pet-feeder"

Paste in the YAML from the attached file, change the WIFI credentials and IP address to something suitable for your network.

Remember to setup the following details in your secrets file:

  • wifi_ssid - your wifi network name
  • wifi_password - your wifi password
  • ota_pwd - a password to protect OTA updates
  • api_pwd - only required if Home Assistant uses a password for ESPHome API access.

Compile the code by clicking the 'upload' button.

This will try to connect to the device over WIFI and will fail. This is perfectly ok at this stage.

substitutions:
  devicename: spf-1010-ty-pet-feeder
  friendly_name: Small Cat Feeder

esphome:
  name: '${devicename}'
  platform: ESP8266
  board: esp_wroom_02

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  power_save_mode: LIGHT
#  use_address: spf-1010-ty-pet-feeder

  
  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:                               
    ssid: $devicename
    password: !secret ap_pwd

captive_portal:

# Enable logging
logger:
  level: WARN
  baud_rate: 0 # disable logging over uart

# Enable Home Assistant API
api:
#  password: !secret api_pwd

ota:
  password: !secret ota_pwd
#-------------------------------------------------------------------------------


################################################################################
# NOTES:
# Motor driver chip is a TC118S.
# INA  INB   MOTOR
#  L    L     Hi-Z
#  L    H     Left
#  H    L     right
#  H    H     Brake
#-------------------------------------------------------------------------------


################################################################################
sensor:
  - platform: wifi_signal
    update_interval: 30s
    id: rssi_sensor
#-------------------------------------------------------------------------------
  - platform: template
    name: "${friendly_name} Wifi"
    unit_of_measurement: "%"
    accuracy_decimals: 0
    icon: "mdi:wifi"
    update_interval: 30s
    lambda: |-
      // Taken from   https://github.com/tzapu/WiFiManager/blob/master/...
      int quality;
      const int rssi = id(rssi_sensor).state;
      if(rssi <= -100){quality = 0;}
      else if (rssi >= -50){quality = 100;}
      else{quality = 2 * (rssi + 100);}
      return quality;
#-------------------------------------------------------------------------------

    
################################################################################
text_sensor:
  - platform: template
    name: "${friendly_name} State"
    update_interval: 1s
    lambda: |-
      if( id(run_motor).is_running() ) { return {"Running"}; }
      else
      if( id(flash_pos_switch_error).is_running() ) { return {"Jammed"}; }      
      else
      return {"Idle"};
#-------------------------------------------------------------------------------
    

################################################################################
switch:
  - platform: gpio          # Motor driver
    pin: GPIO4
    id: motor_drive_a
    restore_mode: ALWAYS_OFF
#-------------------------------------------------------------------------------
  - platform: gpio          # Motor driver
    pin: GPIO5
    id: motor_drive_b
    restore_mode: ALWAYS_OFF
#-------------------------------------------------------------------------------
  - platform: gpio          # RED led (D3)
    pin: GPIO16
    id: led_red
    inverted: true
    restore_mode: ALWAYS_OFF
#-------------------------------------------------------------------------------
  - platform: gpio          # BLUE led (D1)
    pin: GPIO14
    id: led_blue
    inverted: true
    restore_mode: ALWAYS_OFF
#-------------------------------------------------------------------------------


################################################################################
button:
  - platform: template      # Home assistant control
    id: ha_run
    name: "${friendly_name} Run"
    icon: "mdi:cat"
    on_press:
      - logger.log:
          format: "ha_run on_press. Calling script run motor."
          level: DEBUG
      # Do not run if the product is jammed.
      - if:
          condition:
            - script.is_running: flash_pos_switch_error
          then:
            - logger.log:
                format: "ha_run on_press. Cannot run: Jammed."
                level: ERROR
          else:
            - script.execute: run_motor   # Run the motor.

#-------------------------------------------------------------------------------
  - platform: template      # Home assistant control
    id: ha_run_even_when_jammed
    name: "${friendly_name} Run even when jammed"
    icon: "mdi:alert"
    on_press:
      - logger.log:
          format: "ha_run_even_when_jammed on_press. Calling script run motor."
          level: DEBUG

      - script.execute: run_motor   # Run the motor.
#-------------------------------------------------------------------------------



################################################################################
binary_sensor:
  - platform: gpio        # Physical button on unit
    pin:
      number: GPIO0
    id: sw_user_btn
    filters:
      - invert:
      - delayed_on_off: 25ms
    on_press:
      then:
        - logger.log:
            format: "sw_user_btn pressed. Calling script run motor."
            level: DEBUG

        - script.execute: run_motor   # Run the motor.
#-------------------------------------------------------------------------------
  - platform: template
    id: is_motor_running
    on_press:
      then:
        - switch.turn_on: motor_drive_a             # Turn on motor.
        - script.execute: flash_motor_running_led
       
        - logger.log:
            format: "is_motor_running - go."
            level: DEBUG
        
    on_release:
      then:
        - switch.turn_off: motor_drive_a            # Turn off motor.
        - script.stop: flash_motor_running_led
        - switch.turn_off: led_blue

        - logger.log:
            format: "is_motor_running - stop."
            level: DEBUG
#-------------------------------------------------------------------------------
  - platform: gpio
    pin:
      number: GPIO13
    id: sw_motor_position
    filters:
      - delayed_on_off: 25ms
    on_release:
      then:
        - logger.log:
            format: "Position switch released - stopping motor."
            level: DEBUG

        # Stop the motor.
        - binary_sensor.template.publish:
            id: is_motor_running
            state: OFF

        # The position switch is ok, clear any error.
        - script.stop: flash_pos_switch_error
        - switch.turn_on: led_red
#-------------------------------------------------------------------------------
  - platform: gpio
    pin:
      number: GPIO2
    id: unknown_usage
#-------------------------------------------------------------------------------



################################################################################
script:
- id: motor_overrun_protection  # A script to stop the motor if it runs too long
  mode: restart
  then:
    - delay: 5000ms

    # If the motor is still running, an error occurred (position switch faulty or jammed)
    - if:
        condition:
          - binary_sensor.is_on: is_motor_running
        then:
          # Stop the motor.
          - binary_sensor.template.publish:
              id: is_motor_running
              state: OFF
          
          # Flash an error and log an error message.
          - script.execute: flash_pos_switch_error
          - logger.log:
              format: "Motor overrun!! Check motor position switch or clear jam!"
              level: ERROR
#-------------------------------------------------------------------------------
- id: run_motor   # A script that runs the motor. 
                  # This script will be stopped by the motor position switch.
                  # If this script runs for > 4 secs, the motors will be stopped and a error logged.
  
  mode: queued
  then:
    - logger.log:
        format: "run_motor script begin."
        level: DEBUG

    - binary_sensor.template.publish:
        id: is_motor_running
        state: ON
    
    - script.execute: motor_overrun_protection    # Script will restart if it's already running.
    
    # Do nothing until the position switch turns off the motor.
    - wait_until:
        condition:
          - binary_sensor.is_off: is_motor_running

    - logger.log:
        format: "run_motor script complete."
        level: DEBUG
#-------------------------------------------------------------------------------
- id: flash_motor_running_led
  mode: restart
  then:
    while:
      condition:
        lambda: |-
          return true;
      then:
        - switch.turn_on: led_blue
        - delay: 250ms
        - switch.turn_off: led_blue
        - delay: 250ms
#-------------------------------------------------------------------------------
- id: flash_pos_switch_error
  mode: restart
  then:
    while:
      condition:
        lambda: |-
          return true;
      then:
        - switch.turn_on: led_red
        - delay: 150ms
        - switch.turn_off: led_red
        - delay: 150ms
#-------------------------------------------------------------------------------
- id: flash_wifi_error
  mode: restart
  then:
    - script.wait: flash_pos_switch_error     # position sensor error takes priority over this script.
    - while:
        condition:
          lambda: |-
            return true;
        then:
          - switch.turn_on: led_red
          - delay: 500ms
          - switch.turn_off: led_red
          - delay: 500ms
#-------------------------------------------------------------------------------



################################################################################
interval:
  # Check wifi is connected.
  - interval: 2s
    then:
      - script.wait: flash_pos_switch_error     # position sensor error takes priority over this script.
      - if:
          condition:
            wifi.connected:
          then:
            - script.stop: flash_wifi_error
            - if:
                condition:
                  switch.is_off: led_red
                then:
                  - switch.turn_on: led_red
          else:
            - script.execute: flash_wifi_error
#-------------------------------------------------------------------------------

Step 4: Programming of the PCB for the 1st Time

Next we must program the PCB. This is done with the USB to Serial converter. Once this has been done successfully, further future updates can be done wirelessly, Over the Air!

  1. Download and run https://github.com/esphome/esphome-flasher/release....
  2. Navigate to the output directory, and copy the *.bin file.
    For my install, the output bin file is:
    \spf-1010-ty-pet-feeder\.pioenvs\spf-1010-ty-pet-feeder\firmware.bin
    This can be found by looking at the compile output (see photo).
  3. Press and hold the button on the PCB and at the same time, apply power to the board.
    Remember, 3 Volts only.
  4. Keep holding the button, and start the programming process from ESPHome Flasher.

Once the board has started programming, and you see progress in the ESPHome Flasher application output window, you can (optionally) release the push button.

If ESPHome Flasher displays an error when trying to connect to the device, try swapping over TX and RX and try again.

After successful programming, disconnect and re-apply power.

You should see the red LED light up as follows:

  • Flashing: Attempting to connect to WIFI.
  • Solid on: Connected to WIFI.

If the LED remains flashing for more than 10 seconds, check the WIFI settings in the YAML code is correct for your network, modify, build, and reprogram as described above.

When the red LED is solid on, the next stage is to program the device Over The Air (OTA).

Step 5: Programming the PCB - Over the Air (OTA)

In the ESPHome IDE, click the upload button. This will now attempt to program the board over the air, via WIFI.

If the upload was successful (see photo), then you can reprogram the board at any time wirelessly, so there's no need for a physical connection to the board.

You can disconnect the USB to Serial converter and reassemble the feeder.

Step 6: Using the Feeder

The operation of the feeder is as follows:

LEDs:

  • RED:
    • Flashing slowly - Connecting to WIFI
    • Solid - Connected to WIFI
    • Flashing fast - feeder is jammed or the motor position switch is broken
  • BLUE:
    • Flashing - Dispensing food.

Button Operation:

Press the button to dispense one portion of food. Multiple presses will dispense multiple portions.

If the feeder is jammed (red LED flashing fast), try pressing the button once to see if the feeder un-jams. If not, you'll have to clear it out manually.

Don't try to run the feeder if it is jammed; you'll damage the gears or burn out the electronics.

If the product is showing as jammed, but is still dispensing food, then the motor position switch is broken and should be replaced.

Step 7: Home Assistant - ESPHome Integration

The feeder will add an ESPHome device into the Configuration -> Devices screen in Home Assistant:

It is named $device_name as per the YAML. Feel free to change the $device_name in the YAML to suit your use-case.

For the images below,$device_name was 'spf-1010-ty-pet-feeder'.

By clicking 'entities', we can see there are 3 entities created. They are prefixed with the $friendly_name from the YAML. Feel free to change the $friendly_name in the YAML to suit your use-case.

For the images below, the friendly name was 'Small Pet Feeder'.

Entities:

  • Small Pet Feeder Run - This is a switch, it dispenses one portion and will automatically turn off once that portion has been dispensed.
  • Small Pet Feeder Run even when jammed - This is run the feeder, even if it's in the Jammed state. Use with caution.
  • Small Pet Feeder Wifi- The WIFI signal strength.

  • Small Pet Feeder State - The status of the device, this can be one of the following:

    • Idle - Not running
    • Running - Food is being dispensed
    • Jammed - The feeder is jammed.

If the status is jammed, the feeder will no longer respond to run switch presses from home assistant. You must unjam the feeder and press the physical button on the unit or power cycle the device to allow Home Assistant to run the feeder again. This stops automatic feeding from Home Assistant causing damage to the device, or even burning up the electronics, potentially causing a fire.

Step 8: Home Assistant - Scripting

The best way to operate the device is by running a script in Home Assistant. This allows correct timing to be achieved and allows the script to be called by Automations, Alexa, Google Home or other Home Assistant Integrations.

To run the device and dispense a 'single portion', the following steps must occur:

  1. Trigger the 'Run' switch.
  2. Wait for the 'Run' switch to turn of.
  3. Wait another second before any retriggering (e.g. for extra portions etc)

Multiple portions can then be dispensed by calling the 'single portion' script multiple times!

Step 9: Home Assistant - Scripting - Single Portion

Here's an example of the 'single portion' script. Note the script is "Queued", allowing it to be called multiple times for multiple portions (this script is set to max of 10, change as required).
Click on the images to see the configuration. Home Assistant YAML:

feed_the_cat_one_portion:
  sequence:
  - type: turn_on
    entity_id: switch.small_cat_feeder_run
    domain: switch
  - wait_for_trigger:
    - platform: device
      type: turned_off
      entity_id: switch.small_cat_feeder_run
      domain: switch
      for:
        hours: 0
        minutes: 0
        seconds: 1
        milliseconds: 0
    continue_on_timeout: false
    timeout: 00:00:10
  mode: queued
  icon: mdi:cat
  alias: Feed the cat one portion
  max: 10

Step 10: Home Assistant - Scripting - Multiple Portions

Here's an example of the 'multiple portions' script.

This script simply calls the 'single portion' script multiple times. This is useful so you can create a script for each number of portions required, e.g. breakfast (3 portions), lunch (1 portion), dinner (2 portions), evening snack, midnight feast etc.. and each script simply calls the 'one portion' script the required number of times.

The individual scripts can then be called from Alexa, etc. "Alexa, feed the cat lunch", or from other triggers in Home Assistant.

Click on the images to see the configuration. Home Assistant YAML:

(this script will dispense 2 portions)

feed_the_cat_two_portions:
  sequence:
  - repeat:
      count: '2'
      sequence:
      - service: script.feed_the_cat_one_portion
  mode: queued
  icon: mdi:cat
  alias: Feed the cat two portions
  max: 10

Step 11: Home Assistant - Automations

Home Assistant can be configured to automaticly call the previously configured scripts. This is great for feeding your pet automaticly.

Click on the images to view the automation setup.

This automation feeds my cat twice a day, at 5:30am and 4:30pm, two portions per meal.

Home Assistant YAML:

- alias: Feed the cat (breakfast and dinner)
  description: 2 portions
  trigger:
  - platform: time
    at: 05:30
  - platform: time
    at: '16:30'
  condition: []
  action:
  - repeat:
      count: '2'
      sequence:
      - service: script.feed_the_cat_one_portion
  mode: single

And this automation gives my cat a midnight snack (one portion):

- alias: Feed the cat (midnight snack)
  description: 1 portion
  trigger:
  - platform: time
    at: '00:30'
  condition: []
  action:
  - repeat:
      count: '1'
      sequence:
      - service: script.feed_the_cat_one_portion
  mode: single<br>

Step 12: Home Assistant - Running, Even If Jammed (!)

If the feeder jams, Home Assistant will no longer be able to dispense food when the "Small Pet Feeder Run" button is pressed, so don't rely on this if you're away from home. The chances of the product jamming are small, but possible.

If you want to try and run the feeder, even when jammed, use the "Small Pet Feeder Run even when jammed" button.

A better way would be to set up home assistant to alert you if the status changed to 'Jammed'; then call a friend to go round your house and fix it, but it's up to you.

Step 13: Final Thoughts

This is a relatively cheap pet feeder. It's available rebranded by many different companies so should be obtainable in your own country.

The software YAML is simple to understand and doesn't do anything that complex, so it should be easy for you to customise the code to what you'd like.

I have mine connected to Alexa via Home Assistant, but mainly use it to automaticly dispense food for the cat twice a day, and it's been working now for a few months (May 2021) reliably enough.

For those that are adventurous, the ESP device has an ADC input that's not used. By rigging up a 2.2k and 10k resistor to produce a maximum 1 Volt input to the ADC, you could monitor the voltage of the battery backup.

Let me know if you manage to do this, that would be cool.

All the best, happy hacking!