Petkit fresh element setup, device has no entities

I’m trying to get a Petkit Fresh Element Solo setup with updated esphome firmware. I’m following the instructions here: Petkit Fresh Element Solo Pet Feeder | ESPHome Devices, but using the config from the linked github (esphome-configs/petkit-fresh-element-solo.yaml at b14fbd86645059c7ea9a029f742862ad6fdae8f2 · n6ham/esphome-configs · GitHub).

The flash process completes, the device shows up in home assistant but there’s no entities. any thoughts?

Deviations from referenced yaml:

  • in wifi I added ssid and password
  • in esphome I added project info (name and version)

The esphome block looks like this now:

esphome:
  name: $name
  friendly_name: $friendly_name
  project:
    name: petkit.solo
    version: "1.0"
  name_add_mac_suffix: $name_add_mac_suffix
  comment: $device_description
  on_boot:
    - light.turn_on:
        id: led
        effect: fast_blink

My esphome version returns Version: 2025.12.5.

I have now enabled logging and I’m getting Reading failed CONNECTION_CLOSED errno=128

Issued aioesphomeapi-logs 192.168.0.205

2026-01-11 12:20:07.544 INFO     Successfully resolved 192.168.0.205 in 0.000s
2026-01-11 12:20:07.658 INFO     Successfully connected to 192.168.0.205 in 0.114s
2026-01-11 12:20:07.683 INFO     Successful handshake with fresh-element-solo-2382e4 @ 192.168.0.205 in 0.025s
[12:20:07.701][I][app:194]: ESPHome version 2025.12.5 compiled on Jan 11 2026, 12:15:03
[12:20:07.701][I][app:196]: Project petkit.solo version 1.0
[12:21:33.904][W][api.connection:1978]: aioesphomeapi (192.168.0.97): Reading failed CONNECTION_CLOSED errno=128

with a direct connection, esphome logs petkit-fresh-element-solo.yaml:

[12:42:25.730]rst:0x1 (POWERON_RESET),boot:0x16 (SPI_FAST_FLASH_BOOT)
[12:42:25.732]configsip: 0, SPIWP:0xee
[12:42:25.738]clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
[12:42:25.740]mode:DIO, clock div:2
[12:42:25.742]load:0x3fff0030,len:6380
[12:42:25.744]ho 0 tail 12 room 4
[12:42:25.747]load:0x40078000,len:15916
[12:42:25.749]load:0x40080400,len:3860
[12:42:25.751]entry 0x40080630
[12:42:25.757]I (29) boot: ESP-IDF 5.5.1 2nd stage bootloader
[12:42:25.761]I (29) boot: compile time Jan 11 2026 11:16:36
[12:42:25.764]I (29) boot: Multicore bootloader
[12:42:25.767]I (30) boot: chip revision: v1.1
[12:42:25.770]I (33) boot.esp32: SPI Speed      : 40MHz
[12:42:25.774]I (37) boot.esp32: SPI Mode       : DIO
[12:42:25.778]I (40) boot.esp32: SPI Flash Size : 8MB
[12:42:25.782]I (44) boot: Enabling RNG early entropy source...
[12:42:25.785]I (49) boot: Partition Table:
[12:42:25.791]I (51) boot: ## Label            Usage          Type ST Offset   Length
[12:42:25.798]I (58) boot:  0 nvs              WiFi data        01 02 00009000 00005000
[12:42:25.804]I (64) boot:  1 otadata          OTA data         01 00 0000e000 00002000
[12:42:25.810]I (71) boot:  2 app0             OTA app          00 10 00010000 001c0000
[12:42:25.817]I (77) boot:  3 app1             OTA app          00 11 001d0000 001c0000
[12:42:25.824]I (84) boot:  4 eeprom           Unknown data     01 99 00390000 00001000
[12:42:25.830]I (90) boot:  5 spiffs           Unknown data     01 82 00391000 0000f000
[12:42:25.833]I (97) boot: End of partition table
[12:42:25.841]I (100) esp_image: segment 0: paddr=00010020 vaddr=3f400020 size=21670h (136816) map
[12:42:25.884]I (154) esp_image: segment 1: paddr=00031698 vaddr=3ffb0000 size=03f0ch ( 16140) load
[12:42:25.892]I (160) esp_image: segment 2: paddr=000355ac vaddr=40080000 size=0aa6ch ( 43628) load
[12:42:25.907]I (178) esp_image: segment 3: paddr=00040020 vaddr=400d0020 size=c0270h (787056) map
[12:42:26.177]I (447) esp_image: segment 4: paddr=00100298 vaddr=4008aa6c size=09e54h ( 40532) load
[12:42:26.192]I (463) esp_image: segment 5: paddr=0010a0f4 vaddr=50000000 size=00020h (    32) load
[12:42:26.200]I (473) boot: Loaded app from partition at offset 0x10000
[12:42:26.206]I (473) boot: Disabling RNG early entropy source...
[12:42:26.242][I][logger:121]: Log initialized

Turn on debugging in verbose mode. Post a little more of the compile log, the bit from the beginning to where the run part also shows for a few errors. [Formatted </> please}
Did you post all your yaml? Looks a little shorter than the one you linked to on GitHub, which is now two years old.
Did you apply all the latest updates?

The compile log was too long to post, I’ll try to attach. I’ll try getting VERBOSE log. In the meantime, below is my petkit-fresh-element-solo_jvc.yaml (the logger update is now changed).

substitutions:
  name: fresh-element-solo
  friendly_name: Petkit Fresh Element Solo
  device_description: "Petkit Fresh Element Solo Pet Feeder"
  default_scoops: "1"
  min_scoops: "0"
  max_scoops: "6"
  name_add_mac_suffix: "true"

esphome:
  name: $name
  friendly_name: $friendly_name
  project:
    name: petkit.solo
    version: "1.0"
  name_add_mac_suffix: $name_add_mac_suffix
  comment: $device_description
  on_boot:
    - light.turn_on:
        id: led
        effect: fast_blink

esp32:
  board: esp32dev
  framework:
    type: arduino

api:

ota:
  platform: esphome

logger:
  level: VERBOSE
  #level: INFO
  #baud_rate: 0

web_server:
  port: 80

wifi:
  #ap: {} # This spawns an AP with the device name and mac address with no password.
  ssid: "SECRET"
  password: "SECRET"

captive_portal:

dashboard_import:
  package_import_url: github://n6ham/esphome-configs/petkit-fresh-element-solo.yaml

globals:
  - id: scoops_count
    type: int
  - id: max_scoops
    type: int
  - id: food_sensor_count
    type: int
  - id: last_food_sensor_count
    type: int
    initial_value: "0"
    restore_value: True
  - id: last_food_scoops_count
    type: int
    initial_value: "0"
    restore_value: True

script:
  - id: play_rtttl
    parameters:
      song_str: string
    then:
      - if:
          condition:
            lambda: return !(id(mute_sounds).state);
          then:
            - rtttl.play:
                rtttl: !lambda "return song_str;"
  - id: actuate_feeder
    parameters:
      scoops: int
    then:
      - if:
          condition:
            lambda: return scoops > 0;
          then:
            - logger.log:
                level: INFO
                format: "Serving %d scoops"
                args: [scoops]
            - lambda: |-
                id(play_rtttl)->execute("two_shorts:d=4,o=5,b=100:16e6,16e6");
                id(scoops_count) = 0;
                id(food_sensor_count) = 0;
                id(max_scoops) = scoops;
            - switch.turn_on: feed_forward
  - id: dispatch_feeder_food_dispensed_event
    parameters:
      event_message: string
    then:
      - homeassistant.event:
          event: esphome.feeder_food_dispensed
          data:
            message: !lambda return event_message;
  - id: dispatch_feeder_food_low_event
    parameters:
      event_message: string
    then:
      - homeassistant.event:
          event: esphome.feeder_food_low
          data:
            message: !lambda return event_message;
  - id: check_food_level
    parameters:
      food_dispensed: bool
      play_sound: bool
      send_event: bool
    then:
      lambda: |-
        if (id(last_food_scoops_count) > 0 && id(last_food_sensor_count) / id(last_food_scoops_count) < id(low_food_threshold).state) {
          if (play_sound) {
            id(play_rtttl)->execute("siren:d=8,o=5,b=100:d,e,d,e,d,e,d,e");
          }
          if (send_event) {
            id(dispatch_feeder_food_low_event)->execute("${friendly_name} food level is low. Check the hopper.");
          }
          id(feeder_state).publish_state("Food level is low");
          ESP_LOGI("main", "Food level is low");
        } else if (food_dispensed) {
          id(feeder_state).publish_state("Food dispensed");
          id(play_rtttl)->execute("one_short:d=4,o=5,b=100:16e6");
          if (id(last_food_scoops_count) == 1) {
            id(dispatch_feeder_food_dispensed_event)->execute("${friendly_name} dispensed 1 scoop of food.");
          } else {
            id(dispatch_feeder_food_dispensed_event)->execute("${friendly_name} dispensed " + to_string(id(scoops_count)) + " scoops of food.");
          }
        }

light:
  - platform: binary
    id: led
    output: led_output
    effects:
      - strobe:
          name: fast_blink
          colors:
            - state: True
              duration: 125ms
            - state: False
              duration: 125ms
    internal: True

output:
  - id: led_output
    platform: gpio
    pin: GPIO5
  - platform: ledc
    pin: GPIO16
    id: rtttl_output

rtttl:
  output: rtttl_output

interval:
  - interval: 1s
    then:
      if:
        condition:
          wifi.connected:
        then:
          - light.turn_on:
              id: led
              effect: None
        else:
          - light.turn_on:
              id: led
              effect: fast_blink

uart:
  tx_pin: GPIO1
  rx_pin: GPIO3
  baud_rate: 9600

number:
  - platform: template
    id: default_scoops
    name: "Manual dispense scoops"
    icon: mdi:cup
    entity_category: config
    min_value: 1
    max_value: $max_scoops
    initial_value: 1
    optimistic: true
    step: 1
    restore_value: true
    unit_of_measurement: scoops
    mode: slider
  - platform: template
    id: low_food_threshold
    # Minimum food quantity per scoop (specific to a particular dry food).
    name: "Low food threshold"
    icon: mdi:cup-outline
    entity_category: config
    min_value: 1
    max_value: 10
    initial_value: 5
    optimistic: true
    step: 1
    restore_value: true
    mode: slider
  - platform: template
    id: schedule_cups_0000
    name: "00:00 cups"
    icon: mdi:cup
    entity_category: config
    min_value: $min_scoops
    max_value: $max_scoops
    initial_value: 0
    optimistic: true
    step: 1
    restore_value: true
    unit_of_measurement: scoops
    mode: slider
  - platform: template
    id: schedule_cups_0100
    name: "01:00 cups"
    icon: mdi:cup
    entity_category: config
    min_value: $min_scoops
    max_value: $max_scoops
    initial_value: 0
    optimistic: true
    step: 1
    restore_value: true
    unit_of_measurement: scoops
    mode: slider
  - platform: template
    id: schedule_cups_0200
    name: "02:00 cups"
    icon: mdi:cup
    entity_category: config
    min_value: $min_scoops
    max_value: $max_scoops
    initial_value: 0
    optimistic: true
    step: 1
    restore_value: true
    unit_of_measurement: scoops
    mode: slider
  - platform: template
    id: schedule_cups_0300
    name: "03:00 cups"
    icon: mdi:cup
    entity_category: config
    min_value: $min_scoops
    max_value: $max_scoops
    initial_value: 0
    optimistic: true
    step: 1
    restore_value: true
    unit_of_measurement: scoops
    mode: slider
  - platform: template
    id: schedule_cups_0400
    name: "04:00 cups"
    icon: mdi:cup
    entity_category: config
    min_value: $min_scoops
    max_value: $max_scoops
    initial_value: 0
    optimistic: true
    step: 1
    restore_value: true
    unit_of_measurement: scoops
    mode: slider
  - platform: template
    id: schedule_cups_0500
    name: "05:00 cups"
    icon: mdi:cup
    entity_category: config
    min_value: $min_scoops
    max_value: $max_scoops
    initial_value: 0
    optimistic: true
    step: 1
    restore_value: true
    unit_of_measurement: scoops
    mode: slider
  - platform: template
    id: schedule_cups_0600
    name: "06:00 cups"
    icon: mdi:cup
    entity_category: config
    min_value: $min_scoops
    max_value: $max_scoops
    initial_value: 0
    optimistic: true
    step: 1
    restore_value: true
    unit_of_measurement: scoops
    mode: slider
  - platform: template
    id: schedule_cups_0700
    name: "07:00 cups"
    icon: mdi:cup
    entity_category: config
    min_value: $min_scoops
    max_value: $max_scoops
    initial_value: 0
    optimistic: true
    step: 1
    restore_value: true
    unit_of_measurement: scoops
    mode: slider
  - platform: template
    id: schedule_cups_0800
    name: "08:00 cups"
    icon: mdi:cup
    entity_category: config
    min_value: $min_scoops
    max_value: $max_scoops
    initial_value: 0
    optimistic: true
    step: 1
    restore_value: true
    unit_of_measurement: scoops
    mode: slider
  - platform: template
    id: schedule_cups_0900
    name: "09:00 cups"
    icon: mdi:cup
    entity_category: config
    min_value: $min_scoops
    max_value: $max_scoops
    initial_value: 0
    optimistic: true
    step: 1
    restore_value: true
    unit_of_measurement: scoops
    mode: slider
  - platform: template
    id: schedule_cups_1000
    name: "10:00 cups"
    icon: mdi:cup
    entity_category: config
    min_value: $min_scoops
    max_value: $max_scoops
    initial_value: 0
    optimistic: true
    step: 1
    restore_value: true
    unit_of_measurement: scoops
    mode: slider
  - platform: template
    id: schedule_cups_1100
    name: "11:00 cups"
    icon: mdi:cup
    entity_category: config
    min_value: $min_scoops
    max_value: $max_scoops
    initial_value: 0
    optimistic: true
    step: 1
    restore_value: true
    unit_of_measurement: scoops
    mode: slider
  - platform: template
    id: schedule_cups_1200
    name: "12:00 cups"
    icon: mdi:cup
    entity_category: config
    min_value: $min_scoops
    max_value: $max_scoops
    initial_value: 0
    optimistic: true
    step: 1
    restore_value: true
    unit_of_measurement: scoops
    mode: slider
  - platform: template
    id: schedule_cups_1300
    name: "13:00 cups"
    icon: mdi:cup
    entity_category: config
    min_value: $min_scoops
    max_value: $max_scoops
    initial_value: 0
    optimistic: true
    step: 1
    restore_value: true
    unit_of_measurement: scoops
    mode: slider
  - platform: template
    id: schedule_cups_1400
    name: "14:00 cups"
    icon: mdi:cup
    entity_category: config
    min_value: $min_scoops
    max_value: $max_scoops
    initial_value: 0
    optimistic: true
    step: 1
    restore_value: true
    unit_of_measurement: scoops
    mode: slider
  - platform: template
    id: schedule_cups_1500
    name: "15:00 cups"
    icon: mdi:cup
    entity_category: config
    min_value: $min_scoops
    max_value: $max_scoops
    initial_value: 0
    optimistic: true
    step: 1
    restore_value: true
    unit_of_measurement: scoops
    mode: slider
  - platform: template
    id: schedule_cups_1600
    name: "16:00 cups"
    icon: mdi:cup
    entity_category: config
    min_value: $min_scoops
    max_value: $max_scoops
    initial_value: 0
    optimistic: true
    step: 1
    restore_value: true
    unit_of_measurement: scoops
    mode: slider
  - platform: template
    id: schedule_cups_1700
    name: "17:00 cups"
    icon: mdi:cup
    entity_category: config
    min_value: $min_scoops
    max_value: $max_scoops
    initial_value: 0
    optimistic: true
    step: 1
    restore_value: true
    unit_of_measurement: scoops
    mode: slider
  - platform: template
    id: schedule_cups_1800
    name: "18:00 cups"
    icon: mdi:cup
    entity_category: config
    min_value: $min_scoops
    max_value: $max_scoops
    initial_value: 0
    optimistic: true
    step: 1
    restore_value: true
    unit_of_measurement: scoops
    mode: slider
  - platform: template
    id: schedule_cups_1900
    name: "19:00 cups"
    icon: mdi:cup
    entity_category: config
    min_value: $min_scoops
    max_value: $max_scoops
    initial_value: 0
    optimistic: true
    step: 1
    restore_value: true
    unit_of_measurement: scoops
    mode: slider
  - platform: template
    id: schedule_cups_2000
    name: "20:00 cups"
    icon: mdi:cup
    entity_category: config
    min_value: $min_scoops
    max_value: $max_scoops
    initial_value: 0
    optimistic: true
    step: 1
    restore_value: true
    unit_of_measurement: scoops
    mode: slider
  - platform: template
    id: schedule_cups_2100
    name: "21:00 cups"
    icon: mdi:cup
    entity_category: config
    min_value: $min_scoops
    max_value: $max_scoops
    initial_value: 0
    optimistic: true
    step: 1
    restore_value: true
    unit_of_measurement: scoops
    mode: slider
  - platform: template
    id: schedule_cups_2200
    name: "22:00 cups"
    icon: mdi:cup
    entity_category: config
    min_value: $min_scoops
    max_value: $max_scoops
    initial_value: 0
    optimistic: true
    step: 1
    restore_value: true
    unit_of_measurement: scoops
    mode: slider
  - platform: template
    id: schedule_cups_2300
    name: "23:00 cups"
    icon: mdi:cup
    entity_category: config
    min_value: $min_scoops
    max_value: $max_scoops
    initial_value: 0
    optimistic: true
    step: 1
    restore_value: true
    unit_of_measurement: scoops
    mode: slider

time:
  - id: sntp_time
    platform: sntp
    on_time:
      # Hourly
      - hours: 7-22
        minutes: 0
        seconds: 0
        then:
          - lambda: |-
              id(check_food_level)->execute(/* food_dispensed = */ false, /* play_sound = */ true, /* send_event = */ false);
      - hours: "*"
        minutes: 0
        seconds: 0
        then:
          - lambda: |-
              auto hour = id(sntp_time).now().hour;
              switch (hour) {
                case 0: id(actuate_feeder)->execute((int) id(schedule_cups_0000).state); break;
                case 1: id(actuate_feeder)->execute((int) id(schedule_cups_0100).state); break;
                case 2: id(actuate_feeder)->execute((int) id(schedule_cups_0200).state); break;
                case 3: id(actuate_feeder)->execute((int) id(schedule_cups_0300).state); break;
                case 4: id(actuate_feeder)->execute((int) id(schedule_cups_0400).state); break;
                case 5: id(actuate_feeder)->execute((int) id(schedule_cups_0500).state); break;
                case 6: id(actuate_feeder)->execute((int) id(schedule_cups_0600).state); break;
                case 7: id(actuate_feeder)->execute((int) id(schedule_cups_0700).state); break;
                case 8: id(actuate_feeder)->execute((int) id(schedule_cups_0800).state); break;
                case 9: id(actuate_feeder)->execute((int) id(schedule_cups_0900).state); break;
                case 10: id(actuate_feeder)->execute((int) id(schedule_cups_1000).state); break;
                case 11: id(actuate_feeder)->execute((int) id(schedule_cups_1100).state); break;
                case 12: id(actuate_feeder)->execute((int) id(schedule_cups_1200).state); break;
                case 13: id(actuate_feeder)->execute((int) id(schedule_cups_1300).state); break;
                case 14: id(actuate_feeder)->execute((int) id(schedule_cups_1400).state); break;
                case 15: id(actuate_feeder)->execute((int) id(schedule_cups_1500).state); break;
                case 16: id(actuate_feeder)->execute((int) id(schedule_cups_1600).state); break;
                case 17: id(actuate_feeder)->execute((int) id(schedule_cups_1700).state); break;
                case 18: id(actuate_feeder)->execute((int) id(schedule_cups_1800).state); break;
                case 19: id(actuate_feeder)->execute((int) id(schedule_cups_1900).state); break;
                case 20: id(actuate_feeder)->execute((int) id(schedule_cups_2000).state); break;
                case 21: id(actuate_feeder)->execute((int) id(schedule_cups_2100).state); break;
                case 22: id(actuate_feeder)->execute((int) id(schedule_cups_2200).state); break;
                case 23: id(actuate_feeder)->execute((int) id(schedule_cups_2300).state); break;
              }

binary_sensor:
  - id: manual_feed_button
    internal: true
    platform: gpio
    pin:
      number: GPIO34
      inverted: true
    on_press:
      then:
        - lambda: id(actuate_feeder)->execute((int) id(default_scoops).state);
  - id: motor_sensor
    internal: true
    platform: gpio
    pin:
      number: GPIO27
      inverted: true
    on_press:
      then:
        - lambda: |-
            id(scoops_count) += 1;
            if (id(scoops_count) >= id(max_scoops)) {
              id(feed_forward).turn_off();
              id(last_food_sensor_count) = id(food_sensor_count);
              id(last_food_scoops_count) = id(scoops_count);
              id(check_food_level)->execute(/* food_dispensed = */ true, /* play_sound = */ true, /* send_event = */ true);
            }
        - logger.log:
            level: INFO
            format: "%d/%d scoops served"
            args: [id(scoops_count), id(max_scoops)]

  - id: feed_sensor
    internal: true
    platform: gpio
    pin:
      number: GPIO14
    on_press:
      then:
        - lambda: |-
            id(food_sensor_count) += 1;

text_sensor:
  - platform: template
    name: "State"
    id: feeder_state
    entity_category: diagnostic

switch:
  - id: enable_sensors
    internal: true
    platform: gpio
    pin:
      number: GPIO33
    restore_mode: ALWAYS_ON
    disabled_by_default: true

  - id: enable_feeder_motor
    internal: true
    platform: gpio
    pin:
      number: GPIO19
    restore_mode: ALWAYS_OFF
    disabled_by_default: true

  - id: feed_forward
    internal: true
    interlock: &interlock_group [feed_forward, feed_reverse]
    platform: gpio
    pin:
      number: GPIO18
    restore_mode: ALWAYS_OFF
    on_turn_on:
      then:
        - switch.turn_on: enable_feeder_motor
    on_turn_off:
      then:
        - switch.turn_off: enable_feeder_motor

  - id: feed_reverse
    internal: true
    interlock: *interlock_group
    platform: gpio
    pin:
      number: GPIO17
    restore_mode: ALWAYS_OFF

  - id: mute_sounds
    name: Mute sounds
    icon: mdi:volume-off
    optimistic: true
    platform: template

sensor:
  - platform: wifi_signal
    name: "Signal"
    update_interval: 60s
  - platform: template
    id: dispensed_food_quantity
    name: "Dispensed food quantity"
    icon: mdi:cup
    entity_category: diagnostic
    state_class: "measurement"
    accuracy_decimals: 0
    lambda: |-
      return id(last_food_sensor_count);
  - platform: template
    id: dispensed_food_scoops
    name: "Dispensed food scoops"
    icon: mdi:cup
    entity_category: diagnostic
    state_class: "measurement"
    accuracy_decimals: 0
    lambda: |-
      return id(last_food_scoops_count);

button:
  - name: "Dispense food"
    id: dispense_food
    icon: mdi:food-turkey
    platform: template
    on_press:
      - lambda: id(actuate_feeder)->execute((int) id(default_scoops).state);
  - platform: restart
    name: "Restart"
    disabled_by_default: true

I posted the compile log over at github: Petkit: after adding device to homeassistant there's no entities. · Issue #3 · n6ham/esphome-configs · GitHub

There were these warnings…

In file included from managed_components\espressif__esp_diagnostics\src\esp_diagnostics_utils.c:52:
C:\Users\jayveesea\.platformio\packages\framework-espidf\components\freertos\esp_additions\include/freertos/task_snapshot.h:8:2: warning: #warning freertos/task_snapshot.h header is no longer used, and will be removed in future versions. [-Wcpp]
    8 | #warning freertos/task_snapshot.h header is no longer used, and will be removed in future versions.
      |  ^~~~~~~

and…

 'virtual void NetworkClient::flush()' is deprecated: Use clear() instead. [-Wdeprecated-declarations]
  616 |   _chunkedClient.flush();
      |   ~~~~~~~~~~~~~~~~~~~~^~
In file included from C:/Users/jayveesea/.platformio/packages/framework-arduinoespressif32/libraries/Network/src/NetworkServer.h:23,
                 from C:/Users/jayveesea/.platformio/packages/framework-arduinoespressif32/libraries/WebServer/src/WebServer.cpp:28:
C:/Users/jayveesea/.platformio/packages/framework-arduinoespressif32/libraries/Network/src/NetworkClient.h:61:8: note: declared here
   61 |   void flush();  // Print::flush tx
      |        ^~~~~

I’ve got verbose debugging now but I’m not seeing any messages.

I have one of these I flashed with ESPHome some time ago, but I’m using a different/simpler config with feeding times hard coded (I briefly considered making an adjustable schedule, but never bothered since I had no intentions of adjusting it regularly). Been incredibly happy with it; even have it acting as a BT Proxy as well.

Can’t help you with whatever specific issue you’re running into with your particular config, but happy to share my current one if you like.

@brooksben11 if you could share your config that would help further investigate this. thx!

I largely copied this from somewhere online when I got mine setup nearly 2 years ago and then tweaked a few things for my use-case (mainly just adding the BT Proxy). Been working great ever since!

esphome:
  name: pet-feeder
  friendly_name: Pet Feeder

esp32:
  board: esp32dev
  framework:
    type: esp-idf

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

ota:
  - platform: esphome
    password: "xxxxxxxxxxxxxxxxxxxxx"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Pet-Feeder Fallback Hotspot"
    password: "xxxxxxxxxxxx"

#Removed for Bluetooth Proxy feature
#captive_portal:

bluetooth_proxy:
#  active: true

esp32_ble_tracker:

button:
  - platform: restart
    name: Restart
  - platform: safe_mode
    name: Restart (Safe Mode)

text_sensor:
  - platform: wifi_info
    ip_address:
      name: IP Address

sensor:
  - platform: wifi_signal
    name: WiFi Strength
    update_interval: 60s
    disabled_by_default: true
  - platform: uptime
    name: Uptime
    disabled_by_default: true

uart:
  rx_pin: GPIO3
  tx_pin: GPIO1
  baud_rate: 9600

binary_sensor:
  - name: "Manual Feed Button"
    id: catfeed1_manual_feed_button
    platform: gpio
    pin: 
      number: GPIO34
      inverted: true
    on_press:
      then:
        - switch.turn_on: catfeed1_feeder_forward

  - name: "Motor Sensor"
    id: catfeed1_motor_sensor
    platform: gpio
    pin: 
      number: GPIO27
      inverted: true
    on_press:
      then:
        - switch.turn_off: catfeed1_feeder_forward
        - switch.turn_off: catfeed1_feeder_reverse
     
  - name: "Infared Feed Sensor"
    id: catfeed1_feed_sensor
    platform: gpio
    pin: 
      number: GPIO14
    filters:
      - delayed_off: 4s

  - name: "WiFi Button"
    id: wifi_button
    platform: gpio
    pin: 
      number: GPIO0
      inverted: true

status_led:
  pin:
    number: GPIO05
    inverted: true

switch:
  - name: "Enable Feeder Motor"
    id: catfeed1_enable_feeder_motor
    platform: gpio
    pin: 
      number: GPIO19
    restore_mode: ALWAYS_OFF
    disabled_by_default: true
    internal: true
    
  - name: "Enable Sensors"
    id: catfeed1_enable_sensors
    platform: gpio
    pin: 
      number: GPIO33
    restore_mode: ALWAYS_ON
    disabled_by_default: true
    internal: true
    
  - name: "Feeder Forward"
    id: catfeed1_feeder_forward
    icon: mdi:bowl 
    platform: gpio
    pin: 
      number: GPIO18
    interlock: &interlock_group [catfeed1_feeder_forward, catfeed1_feeder_reverse]
    restore_mode: ALWAYS_OFF
    interlock_wait_time: 1s
    on_turn_on:
      then:
        - rtttl.play: 'ceot3k:d=4,o=6,b=100:4d6,4e6,4c6,3c5,2g5'
        - switch.turn_on: catfeed1_enable_feeder_motor
        - delay: 3s
        - if:
            condition:
              binary_sensor.is_on: catfeed1_feed_sensor
            then:
              - homeassistant.event:
                  event: esphome.catfeeder_food_dispensed
                  data:
                    message: Food Was Dispensed
              - logger.log: "Food was dispensed!"
            else:
              - homeassistant.event:
                  event: esphome.catfeeder_food_dispensed
                  data:
                    message: Food Was Not Dispensed!
              - logger.log: "Food was not dispensed!"      
              
    on_turn_off:
      then:
        - switch.turn_off: catfeed1_enable_feeder_motor
        
  - name: "Feeder Reverse"
    id: catfeed1_feeder_reverse
    platform: gpio
    pin: 
      number: GPIO17
    interlock: *interlock_group
    restore_mode: ALWAYS_OFF
    interlock_wait_time: 1s
    disabled_by_default: true
    on_turn_on:
      then:
        - switch.turn_on: catfeed1_enable_feeder_motor
    on_turn_off:
      then:
        - switch.turn_off: catfeed1_enable_feeder_motor

  - name: "Buzzer"
    id: buzzer
    icon: mdi:music
    platform: template
    restore_mode: ALWAYS_OFF
    turn_on_action:
      - rtttl.play: 'ceot3k:d=4,o=6,b=100:4d6,4e6,4c6,3c5,2g5'
    turn_off_action:
      - rtttl.stop

output:
  - platform: ledc
    pin: GPIO16
    id: rtttl_out

rtttl:
  output: rtttl_out

time:
  - platform: sntp
    on_time:
      - seconds: 0
        minutes: 0
        hours: 8, 10, 13, 15, 18, 20
        then:
          - switch.turn_on: catfeed1_feeder_forward
1 Like

Thanks for all the help. In the end i just needed to update homeassistant to a newer version. I was previously on 2025.7.1 and I updated to 2025.12.5.