PetKit Custom Integration

Got my Fresh Element Gemini integrated now. Working flawlessly, thank you for sharing.

I do not have a clue about the purposes of ‘number.minimum_eating_duration’ and ‘sensor.average_eating_time’. Can someone explain please?
I mean the feeder itself does not provide any sensors for recognizing a pet and recording its eating duration. Is this meant for capturing every eating duration manually?

Please look at the documentation on github where entities are explained for every supported device.

The Gemini likely has weight sensors in the base where the bowl sits. They can determine how long a pet was eating based on the disturbance of weight in the bowl and how many times that has occurred in a day + how much food is in the bowl. It won’t distinguish which pet ate if you have multiple pets.

Just set up my new Petlibro and about to flash ESPHome. I would appreciate if you could prepare that PR, happy to test it as well!

I have to write documentation for my changes and test the mL estimations some more before I create a PR. You can feel free to use everything else that I have so far…however, aside from the extra functionality, be aware that I have removed some of the options in my yaml that I don’t particularly need (e.g., dashboard_import has been removed since I use my own yaml and don’t want it syncing with what is on the repo).

esphome:
  name: petlibro-water-fountain
  friendly_name: Petlibro Water Fountain
  platformio_options:
    board_build.flash_mode: dio 
    board_build.flash_size: 4MB
    board_build.f_flash: 40000000L
  project:
    name: petlibro.plwf105
    version: "1.0.2"

esp32:
  board: esp32-c3-devkitm-1
  variant: ESP32C3
  framework:
    type: esp-idf


# Enable logging
logger:

# Enable Home Assistant API
api:

ota:

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

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Petlibro-Water-Fountain"
    password: "CHOOSE A PASSWORD"

captive_portal:

esp32_improv:
  authorizer: none

globals:
  - id: max_water_level
    type: int
    initial_value: '10000'
    restore_value: yes
  - id: min_water_level
    type: int
    initial_value: '0'
    restore_value: yes

time:
  - platform: homeassistant
    id: homeassistant_time
    timezone: 'UTC'

datetime:
  - platform: template
    name: "Filter install date"
    id: filter_install_dt
    type: datetime
    optimistic: true
    initial_value:
      year: 2024
      month: 1
      day: 1
      hour: 00
      minute: 01
      second: 01
    restore_value: true
    on_value:
      then:
        - component.update: filter_life_remaining

number:
  - platform: template
    name: "Min Water level"
    id: min_water_num
    optimistic: true
    min_value: -1_000_000_000
    max_value: 1_000_000_000
    initial_value: 0
    step: 1
    restore_value: yes
    on_value:
      then:
        - globals.set:
            id: min_water_level
            value: !lambda 'return int(x);'


  - platform: template
    name: "Max Water level"
    id: max_water_num
    optimistic: true
    min_value: -1_000_000_000
    max_value: 1_000_000_000
    initial_value: 10000
    step: 1
    restore_value: yes
    on_value:
      then:
        - globals.set:
            id: max_water_level
            value: !lambda 'return int(x);'

sensor:
  - platform: pulse_counter
    pin: 
      number: GPIO10
      mode: INPUT
    name: "Pump Error Counter"
    internal: false
    id: errors

  - platform: hx711
    name: "Internal Water Level"
    dout_pin:
      number: GPIO18
      mode: INPUT_PULLUP
    clk_pin:
      number: GPIO19
      mode:
        pullup: true
        output: true
    gain: 128
    update_interval: 3s
    id: water_level
    filters:
      - quantile:
          window_size: 10
          send_every: 1
          send_first_at: 1
          quantile: .9
      - lambda: |-
          if (x <= 0.1) {
            return 0.0;
          } else {
            return x;
          }

  - platform: uptime
    name: Uptime

  - platform: wifi_signal
    name: WiFi Signal
    update_interval: 60s

  - platform: template
    name: Water Level
    id: water_level_percentage
    update_interval: 3s
    state_class: measurement
    unit_of_measurement: '%'
    accuracy_decimals: 1
    lambda: |-
        if(id(water_level).state >= id(max_water_level)) {
          return 100;
        }

        if(id(water_level).state <= id(min_water_level)) {
          return 0;
        }
        
        return ((id(water_level).state - id(min_water_level)) * 100) / (id(max_water_level) - id(min_water_level));

  - platform: template
    name: "Filter life remaining"
    id: filter_life_remaining
    icon: 'mdi:air-filter'
    device_class: duration
    unit_of_measurement: "d"
    accuracy_decimals: 1
    lambda: |-
        int filter_interval = atoi(id(filter_change_interval).state.substr(0,1).c_str());
        // Interval converted to number of days
        int num_days = (filter_interval * 7);
        // Calculate the filter change date in epoch (when filter needs to be changed)
        time_t change_date = id(filter_install_dt).state_as_esptime().timestamp + (num_days * 86400);
        // Calculate num of seconds between current date and filter change date
        int sec_remaining = change_date - id(homeassistant_time).utcnow().timestamp;
        if (sec_remaining <= 0) {
          return 0;
        }
        // Need to multiply by 1.0 so a double is used during rounding
        return round(1.0*sec_remaining / 86400);


web_server:
  
switch:
  - platform: gpio
    name: 'Pump'
    pin: 
      number: GPIO3
      mode: OUTPUT
    restore_mode: ALWAYS_ON

output:
  - platform: gpio
    id: yellow_led_output
    pin:
      number: GPIO6
      mode: OUTPUT

  - platform: gpio
    id: red_led_output
    pin: 
      number: GPIO7
      mode: OUTPUT

light:
  - platform: binary
    name: "Red LED"
    output: red_led_output
    id: red_led

  - platform: binary
    name: "Yellow LED"
    output: yellow_led_output
    id: yellow_led

text_sensor:
  - platform: version
    name: ESPHome Version

  - platform: wifi_info
    ip_address:
      name: IP
    ssid:
      name: SSID
    bssid:
      name: BSSID

binary_sensor:
  - platform: gpio
    name: "WiFi Button"
    pin: 
      number: GPIO1
      mode: INPUT

  - platform: template
    name: "Empty"
    lambda: |-
      return id(errors).state >= 10;
    on_state:
      then:
        - lambda: |-
            if(x) { 
              id(red_led_output).turn_on();
            } else {
               id(red_led_output).turn_off();
            }

button:
  - platform: template
    name: "Set Min Water Level"
    on_press:
      then:
        - number.set:
            id: min_water_num
            value: !lambda 'return id(water_level).state;'

  - platform: template
    name: "Set Max Water Level"
    on_press:
      then:
        - number.set:
            id: max_water_num
            value: !lambda 'return id(water_level).state;'

  - platform: template
    name: "Reset Filter"
    on_press:
      then:
        - datetime.datetime.set:
            id: filter_install_dt
            datetime: !lambda |-
              return id(homeassistant_time).utcnow();

select:
  - platform: template
    name: "Filter change interval"
    id: filter_change_interval
    optimistic: true
    restore_value: true
    options:
      - "2 Weeks"
      - "4 Weeks"
      - "6 Weeks"
      - "8 Weeks"
    initial_option: "2 Weeks"
    on_value:
      then:
        - component.update: filter_life_remaining
1 Like

This looks like a good way to visualize water level in HA:

1 Like

Hi!
I have an Fresh Element Gemini.
If I buy an Eversweet Solo 2, will this work with it?

Hi @RobertD502 ,
I bought a Fresh Element Gemini and it works well if I set the manual feed text input in the device overview. :slight_smile:
Now I want to do this via an automation. I tried with the service “input_text.set_value” but I cannot even find the entity there. If I set the name I get an error in the log
WARNING (MainThread) [homeassistant.helpers.service] Referenced entities text.trockenfutter_automat_manual_feed are missing or not currently available (the name should be correct …)
Now I tried to set the entity directly via the developer tools. This changes the entity but no food is dispensed. After some time it switches back to “0,0”. Any hints how I can make this work with automations?

Thanks a lot for your help and the great work with the integration!

The manual feed entity for the Gemini feeder is a text entity, not an input_text entity. Therefore, you need to use the text.set_value service, not input_text.set_value.

Hope that helps.

1 Like

I believe it will. However, I’d contact petkit and ask them if the gemini supports “remote interconnect” with the Eversweet solo 2.

1 Like

Wow thanks a lot for the fast response! This worked of course immediately.
I was trying this for two hours, I feel so stupid now :rofl:

1 Like

From what I can see in Home Assistant logs it does seem to send passive data but very sporadically, anywhere from (very roughly) 15 - 45 minutes apart. I have found killing the app and reconnecting forces the data to update which makes sense (and is handy for testing) and leads me on to my next question…

Regarding your comment below instead of a BLE integration would it be possible to force or emulate a connection to the app eg every 5 or 10 minutes to get consistent data?

    1. I have not investigated if the fountain advertises its data passively, but I’d assume an active connection is required since, even with petkit’s BLE relay, an active connection to the fountain is made before any data is sent to the relay device. It would also explain why the PetKit app won’t update any fountain data / notify you about the water level being empty unless it is open and establishes an active connection to the water fountain.*

The integration already emulates what the app does: uses another PetKit device as a BLE relay (via the cloud) when connecting remotely. Regarding timing: the next release will include a higher period of time between BLE relay initiations and even a separate option for users to change it if they want to. From my own testing with different polling intervals over the past several months, their water fountains are just inconsistent with data, no way around it. I’ve even encountered using their app to connect directly (no BLE relay) to the water fountain and it states that water is running when physically I can see the water fountain is completely out of water.

1 Like

Where can I find this project to flash a petlibro fountain with esphome?

Any progress on the yumshare integration? Let me know if you need help/tester! Thanks!

Hello @RobertD502

First off - great job making a custom integration for PetKit! I’m choosing a smart litterbox to use and I’m wondering if I should get Petkit PuraMax 2 :slight_smile:

Second off - there are some devices marked as “new” on PetKit’s site:
PetKit PuraMax 2 PETKIT PuraMax 2
Eversweet Max PETKIT EverSweet Max Cordless Smart Pet Drinking Fountain like faucet dockstream water dispenser for cat and dog Portable Cat Waterer Rabbit Waterer
and YumShare feeders.

Do you plan to add support for these devices? If you do, how soon can we expect it? I want to decide for the devices for my cat and you’re the deciding factor! :smiley:

@RobertD502 @wrobelda you using new ESPHome firmware config from @tmfinnell for Petlibro?

UPDATE: See now that @tmfinnell and others been discussing ESPHome for Petlibro products here:

hi is there any update on the petkit yumshare support? i just bought the dual hopper and it doesn’t seem to be picking it up. i have two - a solo and a dual hopper. only the solo is working. thanks

I took some inspiration from your script as well as @RobertD502 's library, plus a metric ton of packet sniffing, and managed build a relatively simple python library around it, to expose most of the controls to MQTT, to enable HA’s MQTT Discovery.

It supports connecting to the W5-family of Petkit Water Fountains via Bluetooth Low Energy, without any Petkit WiFi enabled products.

There’s a few caveats, in terms of the secret being exchanged with the device, and scheduling of Do Not Disturb as well as Lights Out. Hopefully I get around to address the last two - but will have a hard time fixing (correct) generation of secrets, as I only have two devices and thus not nearly enough to reverse engineer the algorithm used.

So, for a bit of shameless self-promotion and plugging, you can find the library here, should it tickle your fancy: PetkitW5BLEMQTT

Hey wow. Cool. Thank you for providing it. I don’t know anything about coding so I used ChatGPT to help. Is what ChatGPT tells me correct?

Beginner-Friendly Guide to Setting Up PetkitW5BLEMQTT in Home Assistant

If you’re new to Home Assistant and not familiar with coding, follow these simple steps to integrate your Petkit W5 device.


1. Preparation

  • Ensure that Home Assistant is running on your device.
  • You’ll need access to the Home Assistant Web Interface and Supervisor.
  • Enable the MQTT Integration in Home Assistant:
    1. Go to SettingsIntegrations in Home Assistant.
    2. Search for MQTT and set it up (if not already configured).

2. Download the Petkit Adapter

  • PetkitW5BLEMQTT is not a ready-to-use add-on, you need to manually set it up:
  • Install the Samba Add-on to enable file sharing between your PC and Home Assistant.
  • Configure Samba, start it, and connect to Home Assistant from your PC. Instructions are available here.

3. Upload the Required Files

  1. Download the repository files from GitHub:
    • On the GitHub page, click CodeDownload ZIP.
    • Extract the ZIP file on your computer.
  2. Upload the entire extracted folder to the Home Assistant config folder:
    • Use Samba to copy the files from your PC, or
    • Use the File Editor add-on in Home Assistant to upload and manage files directly.

4. Configure the Settings

  1. Open the file config.yaml from the uploaded folder.

    • If you’re using the File Editor in Home Assistant, you can edit the file directly.
    • Add your MQTT details and Petkit device information:
      mqtt:
        host: "Your MQTT server address (e.g., Home Assistant IP)"
        port: 1883
        username: "Your MQTT username"
        password: "Your MQTT password"
      
      petkit:
        devices:
          - name: "Petkit Water Fountain"
            mac: "MAC address of your Petkit device"
      
    • MAC address: You can find the MAC address in the Petkit app or your router’s list of connected devices.
  2. Save the file.


5. Start the Petkit Service

  • Currently, you need to run the program manually:
    1. Open Terminal & SSH in Home Assistant (install it via the Add-On Store if necessary).
    2. Run the following command:
      python3 /config/PetkitW5BLEMQTT/petkit_w5_mqtt.py
      
    3. Leave the terminal open so the program keeps running in the background.

6. Display Data in Home Assistant

  1. Add an MQTT sensor to Home Assistant:
    • Go to SettingsDevices & ServicesMQTT.
    • Check if new topics appear (e.g., petkit/water_status).
  2. Configure a sensor manually:
    • Open your configuration.yaml file and add:
      sensor:
        - platform: mqtt
          name: "Petkit Water Level"
          state_topic: "petkit/water_status"
      
  3. Restart Home Assistant to apply the changes.

7. Create Automations

Now that the sensor is available, you can set up automations, such as notifications when the water level is low:

  • Go to SettingsAutomations and create a rule.

I think ChatGPT might have overengineered it a bit :sweat_smile:

I believe these steps, should get you going:

git clone https://github.com/slespersen/PetkitW5BLEMQTT.git
ln -s $(pwd)/PetkitW5BLEMQTT /path/to/your/python/site-packages/PetkitW5BLEMQTT

Change the path to the correct, for your python site-packages directory. You can find the path this way:
python -c "import site; print(site.getsitepackages())"

To find the address of your petkit device, you can run this command:
bluetoothctl scan le

PetKit devices will be prefixed with “Petkit” in the name

Afterwards you run the script this way:
python main.py --address "A1:B2:C3:D4:E5:F6" --mqtt --mqtt_broker "broker.example.com" --mqtt_port 1883 --mqtt_user "user" --mqtt_password "password" --logging_level "INFO"

Remeber to change the arguments to suit your environment.

I’ll be adding a systemd service file in the future, until then the script can be run inside screen to achieve session persistency.

Please note - as the secret is not implemented, you will need to restart the script after the first run, to ensure that the non-compliant secret has been set to the device. This will stop the app from working, until power has been cycled!