devices.esphome.io
Petkit Fresh Element Solo Pet Feeder
Petkit Fresh Element Solo Pet Feeder
Device Type: miscElectrical Standard: globalBoard: esp32Difficulty: Soldering required, 4/5
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.
Internal pinout
Pin | Function |
---|---|
GPIO5 | Status LED |
GPIO16 | RTTTL Buzzer |
GPIO34 | Feed button |
GPIO27 | Feeder motor sensor |
GPIO14 | Food optical sensor |
GPIO19 | Feeder motor control* |
GPIO33 | Feeder sensors control* |
GPIO18 | Feed forward control* |
GPIO17 | Reverse 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 notificationsdescription: ""trigger: - platform: event event_type: esphome.feeder_food_low id: food_low - platform: event event_type: esphome.feeder_food_dispensed id: food_dispensedcondition: []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.