devices.esphome.io

Petkit Fresh Element Solo Pet Feeder

Petkit Fresh Element Solo Pet Feeder

Device Type: misc
Electrical Standard: global
Board: esp32
Difficulty: Soldering required, 4/5

Product Image

Product description

This is an automatic pet feeder for cat/dog sized pets with a proprietary cloud based app. The mainboard has an ESP32-WROOM-32D chip on it and thus can be flashed with an ESPHome firmware to be controlled locally.

The feeder comes with a US power adapter and a barrel connector if ordered from Amazon US, but can be powered by any 5V power adapter with a barrel connector.

Official product page.

Flasing

The feeder base has to be taken apart to access the mainboard. This requires removal of 4 screws at the bottom side, hidden under the rubber feet. The rubber feet has to be partially peeled off to access the screws. See the the wiring diagram for flashing below (borrowed from the Reddit comment). Press and hold the right button while plugging the adapter in, release after a second or two.

Wiring Diagram

Internal pinout

PinFunction
GPIO5Status LED
GPIO16RTTTL Buzzer
GPIO34Feed button
GPIO27Feeder motor sensor
GPIO14Food optical sensor
GPIO19Feeder motor control*
GPIO33Feeder sensors control*
GPIO18Feed forward control*
GPIO17Reverse feed control*

* purpose of some of these pins is unclear

Config

The config below is intended to make the feeder tolerant to the Home Assistant failures, by supporting HA-offline schduling capabaility (but it still requires WiFi and Internet connection to synchronize time with SNTP servers, or the schdule won't work).

It exposes multiple entites that can be used to configure the feeding schedule for every hour of the day.\ User can set a number of scoops to be dispensed at particular hour of a day (if set to 0 - no food will be dispensed).

Food can be also dispensed manually either via hardware button on the right side of the feeder, or via the switch entity.\ The portion size of the manual dispense is configured separately using a slider.

Low food level threshold value allows the feeder to dispatch an esphome.feeder_food_low event with the message containing the firendly feeder name (handy if multiple feeders are in use), when a food sensor counts per scoop gets below the threshold.\ Feeder will also play a siren sound hourly between 7am and 10pm (7 and 22) inclusively, unless sounds are muted via dedicated switch.\ An automation can be configured to listen to the event and notify users about low food level (example is at the end).

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: "false"
esphome:
name: $name
friendly_name: $friendly_name
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
logger:
level: INFO
baud_rate: 0
web_server:
port: 80
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

Automation example

alias: Pet feeder notifications
description: ""
trigger:
- platform: event
event_type: esphome.feeder_food_low
id: food_low
- platform: event
event_type: esphome.feeder_food_dispensed
id: food_dispensed
condition: []
action:
- if:
- condition: trigger
id:
- food_low
then:
- service: notify.notify
metadata: {}
data:
message: "{{ trigger.event.data.message }}"
- service: notify.persistent_notification
metadata: {}
data:
message: "{{ trigger.event.data.message }}"
mode: single

Note: currently, esphome.feeder_food_dispensed event is ignored to not spam the users with multiple notifications about food being dispensed multiple times a day. Only esphome.feeder_food_low will result in persistent notification in Home Assistant and mobile app notifications.

Edit this page on GitHub