A fully local Home Assistant integration for the Petkit Fresh Element Solo (D4) feeder
and the Eversweet Max 2 Cordless (CTW3) water fountain.
Zero bytes sent to Petkit. Zero cloud dependency. Zero telemetry to China.
Petkit's Fresh Element Solo is a decent smart feeder, but it phones home
constantly: heartbeats every 11 seconds over plaintext HTTP to
api.eu-pet.com, permanent MQTT session to
eu-central-prod.mqtt.iotgds.aliyuncs.com (Alibaba Cloud), and a mobile
app that demands location permission just to read your WiFi SSID.
It also has real security issues — unauthenticated remote device control over plain HTTP, no TLS, no integrity checks on BLE provisioning. If you can intercept the HTTP traffic, you can control the feeder.
If you also own the Eversweet Max 2 Cordless (CTW3) fountain, the same problems apply — and the fountain has no Wi-Fi of its own, so it relies on Bluetooth Low-Energy through a paired device (the Petkit app on your phone, or a feeder it shares an account with) to reach the cloud at all. This integration handles both: the D4 acts as transparent BLE relay for the CTW3, so adding the fountain to Home Assistant is optional and requires the D4 feeder to already run this integration. Onboarding is one cloud lookup at setup, then everything runs local. Standalone fountain support (without a D4) is intentionally out of scope — see Related projects for an alternative if that's your case.
So: we replaced the cloud with a local Home Assistant integration that reimplements just enough of the D4 protocol to keep the feeder happy (and BLE-relays for the optional fountain) while never sending a single byte to Petkit or Alibaba.
What happens when you don't intercept:
- Heartbeats every ~11 seconds → ~8,000 per device per day
- State reports every ~14 minutes → ~100 per device per day
- Combined payload: ~850 KB / day / device to Petkit + Alibaba Cloud
- Conservatively 5–10 million Petkit devices deployed globally → 1.5–3 petabytes per year of pet-feeder telemetry, mostly stating the fact that the feeder is online
The volume isn't even the scary part — it's what the payloads contain:
| Field | What it leaks |
|---|---|
ssid, bssid |
Your WiFi name + router MAC → geolocation to ~10 m via WiGLE / Apple / Google WPS databases |
| Timestamps of every feed | Daily rhythm, work schedule, vacation detection |
rsq (WiFi RSSI) |
Movement / device placement inference |
| Multiple Petkit devices on same SSID | Household topology reconstruction |
runtime, firmware version |
Power-outage patterns, update status |
None of this is needed for the feeder to work. This project proves it — the unit runs indefinitely without any of that data leaving the house.
- 📅 Feeding schedule — up to 10 entries per weekday, managed from HA's UI
- 🍽️ Manual feeding — buttons / service for on-demand dispensing
- 🔋 Battery monitoring — level in %, voltage in mV (diagnostic), power source (mains vs. battery)
- 🏠 Full device state — food container, desiccant timer, WiFi signal, firmware version
- 🚨 Error detection — motor error, RTC error, IR sensor, DC errors as binary sensors
- 💾 Persistent — schedule and device state survive HA restarts
- 🌍 Internationalization — English + German out of the box, others via PR
- 🔒 Zero cloud — the feeder never reaches
api.eu-pet.comoraliyuncs.comonce configured

The full device view — controls, sensors, and diagnostic entities, all grouped natively in HA.
![]() Options flow — manage the feeding plan. |
![]() Add-entry form with time picker, portion dropdown, weekdays. |

Optional Lovelace card — drop-in YAML in docs/dashboard_card.yaml.
![]() Eversweet Max 2 (CTW3) — drink counters, pump status, mode select, controls. |
![]() Settings + diagnostics — DND, motion sensors, LED, filter, battery. |
┌───────────────────┐ ┌──────────────────────┐
│ Petkit Fresh │ │ Home Assistant │
│ Element Solo │ ── HTTP (port 80) ─► │ custom_component │
│ (D4, FW 1.267) │ ◄── responses ────── │ emulates cloud │
└───────────────────┘ └──────────────────────┘
▲ │
│ ▼
Feeder is provisioned Feed commands,
via BLE to point at schedule, settings,
your HA server (port 80) error monitoring
The feeder is redirected — via a one-time BLE provisioning, or via a DNS override — to a local HTTP server inside your Home Assistant. That server implements just enough of the Petkit cloud API to keep the feeder happy: heartbeats, device registration, schedule delivery, event reporting, and command pushes.
- Petkit Fresh Element Solo (D4), firmware
1.267— full feature support - Petkit Eversweet Max 2 Cordless (CTW3), firmware
1.11— full feature support, BLE-relayed via the D4 (no WiFi of its own). See Water fountain (optional) below for onboarding.
These feeders speak the same /d4/ protocol — feeding schedule, battery,
heartbeats, everything non-video works exactly the same:
- Petkit YumShare Solo — feeding ✅, camera ❌
- Petkit YumShare Dual-Hopper — feeding ✅ (two-hopper logic untested), camera ❌
Why no camera: Petkit routes video via Agora.io (a commercial WebRTC-SaaS, Chinese infrastructure) with server-side signed tokens. This is a fundamentally different architecture than the plain HTTP we intercept — there is no DNS redirect or local proxy that can replace Agora's signed session broker without access to Petkit's private keys. An existing integration (Jezza34000/homeassistant_petkit) does expose YumShare video in Home Assistant, but the stream physically still transits Petkit + Agora cloud infrastructure — it is not local. If you're a privacy-focused user, that defeats the purpose.
Privacy-respecting alternative: Fresh Element Solo + a separate ONVIF camera (Reolink E1, Tapo C200, anything with RTSP) + Frigate. Costs about the same as a YumShare, gives you 100% local AI monitoring, and lets you replace the camera later independently of the feeder.
- Petkit Mate D2 / D3 — different protocol, different endpoints
- Any Petkit device where the primary function is the camera (Cozy Cam, Cyber pet cameras etc.) — they speak Agora, not D4
- Home Assistant 2024.1 or newer
- A Petkit D4-family feeder
- Ability to run Home Assistant on port 80 (see below)
- A way to redirect the feeder's DNS or IP for
api.eu-pet.comto your Home Assistant (see below)
Via HACS (recommended once merged):
- HACS → Integrations → ⋮ → Custom repositories
- Add
https://github.com/Opcodeffm/petkit-local, category Integration - Install Petkit Local, restart HA
Manually:
- Copy
custom_components/petkit_feeder/to yourconfig/custom_components/ - Restart HA
The feeder's firmware has api.eu-pet.com hardcoded. You must redirect
that hostname, on the feeder's local network, to your HA instance.
Option A — DNS override (recommended): On your router / Pi-hole / UniFi DNS:
- Add an A record:
api.eu-pet.com→<your-HA-ip> - (Optional, belt-and-suspenders) block outbound Internet for the feeder's IP to force it to use your server even if DNS is bypassed.
- (Optional) blackhole
eu-central-prod.mqtt.iotgds.aliyuncs.com— the feeder falls back to HTTP when MQTT is unreachable, which is what we want.
Option B — MITM / NAT:
If you can't do DNS overrides, redirect 203.0.113.X:80 (or whatever
api.eu-pet.com resolves to at that moment) to your HA via firewall NAT.
DNS has multiple A records and they rotate; DNS-override is more
reliable.
The feeder speaks HTTP on port 80 — not configurable on the feeder side. Home Assistant's own frontend usually runs on port 8123, so port 80 is typically free. The integration binds a second aiohttp server on port 80 to serve feeder traffic exclusively.
If something else is already bound to port 80 on your HA host, you
will see OSError: [Errno 98] Address already in use in the log. Stop
the other service (e.g. the NGINX add-on if you're not using SSL
reverse-proxy) and the integration will work.
Case A — feeder is already set up on Petkit's cloud: Once DNS is redirected to your HA, power-cycle the feeder. After ~30s it should reach your local server instead, and the integration auto-detects it — no further config needed.
Case B — fresh out of the box: You need to provision the feeder with your WiFi credentials. Two paths:
- Via the Petkit app (easiest, non-invasive) — set up normally, then do the DNS override, then power-cycle. You'll never need the app again; it phones home only for this initial handshake.
- Via BLE from HA — bypasses Petkit app entirely, using scripts
in the
research/folder of this repo. Requires a bit of technical comfort; seeresearch/README.mdfor the flow.
Settings → Devices & Services → Add Integration → "Petkit Local" → confirm. The feeder will register automatically on its next heartbeat (~10s).
Settings → Devices & Services → Petkit Local → CONFIGURE. A menu appears:
- Add entry — time picker, amount dropdown (10 g / 20 g / 50 g — matches the feeder's hardware granularity), weekday checkboxes, optional name
- Remove entry — pick and delete
- Clear all
- Save and close — pushes to feeder and persists across HA restarts
The schedule lives in Home Assistant, not in the feeder's flash. A
time-change listener fires once per minute and pushes a manual feed
command via the existing queue_feed() path — that way scheduled feeds
arrive even when the D4 firmware is in one of its periodic
heap-watchdog stalls (the feeder catches up with the queued command on
its next heartbeat after recovery).
⚠️ Power-failure caveat. Because the schedule no longer lives in the feeder's flash, the feeder's integrated battery only keeps it connected; it does not keep it feeding if your Home Assistant host loses power. If reliability across power outages matters (it usually does for pet feeders), put your HA host on a small UPS. A ~30 W mini-PC + UPS combo runs for hours on a 600 VA unit.Why we made this trade-off: the feeder's autonomous schedule used to miss feeds when the firmware's BLE-relay heap drift caused a hang. HA-driven scheduling absorbs those hangs. We picked "robust against firmware bugs" over "robust against my power going out", since the latter is solvable with a $50 UPS and the former wasn't solvable at all.
petkit_feeder.set_schedule— replace the full schedulepetkit_feeder.clear_schedule— wipe all entriespetkit_feeder.feed— manually dispense (acceptsamountin grams)petkit_feeder.reset_desiccant— reset the silica-gel timer after changing the packpetkit_feeder.set_food_full— mark the tank as freshly filled (resets the Food remaining (estimated) sensor to 100%)petkit_feeder.set_food_tank_capacity— override the assumed full-tank capacity (default 1700 g)
A ready-to-paste Lovelace card is provided in
docs/dashboard_card.yaml.
Once the integration is in place, the feeder's firmware should never change — our local server always tells it "no update available" (details). If the version does change, something bypassed that protection (e.g. the Petkit app briefly reached the feeder over BLE/LAN, a DNS leak, etc.). Detect it with this automation:
# configuration.yaml (or Settings → Automations → "Edit in YAML")
alias: "Petkit — firmware changed (unexpected!)"
description: "Firmware is pinned by the local integration. A change means something reached Petkit's cloud."
trigger:
- platform: state
entity_id: sensor.petkit_feeder_firmware # adjust to YOUR entity ID
# Fires on any state change …
condition:
# … except the normal "unknown → <version>" on first load after restart.
- condition: template
value_template: "{{ trigger.from_state.state not in ['unknown', 'unavailable', ''] }}"
action:
- service: persistent_notification.create
data:
title: "⚠️ Petkit firmware changed"
message: >-
Firmware changed from {{ trigger.from_state.state }}
to {{ trigger.to_state.state }} at {{ now() }}.
This should never happen with the local integration —
check FIRMWARE_PROTECTION.md in the repo.
notification_id: petkit_firmware_change
# Optional: also push to mobile app
# - service: notify.mobile_app_your_phone
# data:
# title: "⚠️ Petkit firmware changed"
# message: "{{ trigger.from_state.state }} → {{ trigger.to_state.state }}"
mode: singleReplace sensor.petkit_feeder_firmware with your actual entity ID
(find it under Settings → Devices & Services → Petkit Local →
the device → Firmware diagnostic entity).
The integration also supports the Petkit Eversweet Max 2 Cordless (CTW3) water fountain. The fountain has no WiFi of its own — it relays everything through the D4 over Bluetooth. After a one-time onboarding step, the fountain runs fully locally: same as the feeder, no cloud calls during normal operation.
Each fountain has a secret that Petkit's cloud generates during
factory pairing. The fountain validates this secret on every BLE
session opened by the D4. There is no way to extract the secret from
the fountain itself (we tested), so we have to retrieve it from
Petkit's cloud once, store it locally, and never call the cloud again
for fountain operation.
After onboarding, the only ongoing cloud interaction is a daily
/user/refreshsession call at 03:30 local time to keep the session
token alive. If the cloud goes away entirely, your fountain keeps
working — only adding new fountains would break.
Settings → Devices & Services → Petkit Local → Configure → Add water fountain. Pick one of three authentication paths:
If you signed up to your Petkit account with email + password, just
enter them. We call /user/login once, store the resulting session
token locally, and continue. Your password is not stored — only its
MD5 hash is sent to Petkit (per their API), and we discard it after the
login call returns.
If you signed in via Apple/Google/WeChat, you don't have a password.
Instead, capture your X-Session token once and paste it. The token
ages in the same way as a normal session, so daily refresh keeps it
alive forever.
To capture the token:
- Easiest (Mac/Linux laptop): Run
mitmproxy --listen-port 8080, set your iPhone's Wi-Fi proxy to your laptop:8080, install themitmproxy-ca-cert.pemprofile on the phone, open the Petkit app, pull-to-refresh on the device list. In mitmproxy, find any request toapi.eu-pet.com, look for theX-Sessionheader, copy the value. - iOS app alternative: Install HTTP Catcher
(free, no jailbreak), enable HTTPS decryption, install its certificate
profile, open Petkit. Find any
api.eu-pet.comrequest and copy theX-Sessionheader. - Android app alternative: HttpCanary works similarly.
The token is a 52-character string. Paste it into the form, hit Submit, done.
If you already have a fountain's id, mac, and secret from your own
prior packet capture, you can enter them directly. No cloud calls at
all in this path. Useful for power users and for testing.
Whichever path you took, the next step asks for the fountain's
MAC address and serial number. Both are visible in the official
Petkit app under Device → Settings → Info. Enter them, hit Submit,
and the integration calls /ctw3/signup once to fetch everything else.
A confirmation card shows the fetched device data (with the secret
truncated for safety). Click Add this fountain and the integration
reloads, exposing the fountain's entities.
- We don't auto-discover fountains. You provide MAC + SN. This is by
design — Petkit's
/discovery/device_roster_v2returned empty for our test account, and/ctw3/owndevicesis similarly unreliable. MAC + SN copied from the app is the single canonical path. - We don't pair new fountains. Pair via the official Petkit app first; our integration onboards an already-paired fountain into HA.
- We don't write any cloud configuration. All settings (mode, brightness, sleep timers, motion sensors, etc.) are sent over BLE via the D4 relay.
maksym-pasichnyk/PetKit-Eversweet-Max
takes the alternative architecture: phone-as-BLE-proxy directly to the
CTW3, no D4 involved. Different trade-offs — useful if you only have a
fountain (no D4 feeder), but requires keeping a phone-style BLE proxy
running 24/7.
- After HA restart: the feeder sends a state-report roughly every
14 minutes. Until the first one arrives, Firmware / WiFi / Battery
might show
unknown. This is normal; the integration persists the last known values across restarts. - First manual feed after cold start: ~15–25 seconds delay. The heartbeat is on an 11s poll cycle, and the motor itself takes a few seconds. This is not a bug.
- RTC error on boot (
rtc_c: 1): can happen if the feeder has been without power for a while. Our integration transmits time via the heartbeat response; the feeder resynchronizes within about an hour. - "Running on battery" vs "On mains": the
ubatfield in the state-report toggles; thepower_sourcesensor reflects this live.
| Entity | Purpose |
|---|---|
binary_sensor.*_verbindung / _connection |
Online / offline (stale-detection after 180s) |
binary_sensor.*_motor_fehler / _motor_error |
Motor jammed / failed |
binary_sensor.*_rtc_fehler / _rtc_error |
Feeder has no valid time (schedule won't run!) |
binary_sensor.*_ir_sensor_fehler (opt-in) |
IR-sensor error (food detection) |
binary_sensor.*_dc_fehler (opt-in) |
DC-input error |
sensor.*_futterungsplan / _feed_schedule |
Human-readable schedule summary + raw attributes |
sensor.*_batterie / _battery |
Battery level in % (5× AA, 6500–8000 mV range) |
sensor.*_batteriestatus / _battery_status |
OK / Niedrig / Running on battery / No batteries |
sensor.*_stromquelle / _power_source |
Mains (USB/DC) vs. battery |
sensor.*_futterbehalter / _food_container |
OK / low / empty / unknown (firmware ENUM, not a percentage) |
sensor.*_food_remaining_estimated |
Estimated tank fill in % (counts grams since last refill against tank_capacity_g, default 1700; auto-resets when firmware reports a refill, manual reset via set_food_full) |
(diagnostic, opt-in) sensor.*_food_remaining_grams |
Same as above but in grams |
sensor.*_trockenmittel_tage_ubrig / _desiccant_days_left |
0–30 day counter |
sensor.*_futterungen_heute / _feedings_today |
Today's feeding count |
sensor.*_futtermenge_heute / _food_amount_today |
Today's dispensed grams |
(diagnostic) sensor.*_firmware |
Firmware version |
(diagnostic) sensor.*_wifi_signal |
WiFi RSSI in dBm |
(diagnostic) sensor.*_letzter_heartbeat / _last_heartbeat |
Timestamp |
(diagnostic) sensor.*_laufzeit / _uptime |
Feeder uptime |
(diagnostic, opt-in) sensor.*_batteriespannung |
Raw mV |
(diagnostic, opt-in) sensor.*_ram_frei |
ESP32 free heap |
switch.*_futterungston / _feed_sound |
Feeding sound on/off |
switch.*_tastensperre / _manual_lock |
Physical button lock |
switch.*_led_anzeige / _led_display |
LED on/off |
button.*_futtern_* / _feed_* |
Manual feed buttons (10 g / 20 g / 50 g) |
button.*_trockenmittel_zurucksetzen / _reset_desiccant |
Reset silica-gel counter |
See SECURITY.md for the vulnerabilities we discovered in Petkit's cloud and BLE protocols. These are real issues that motivated this project. If you're planning to request CVE IDs referencing these findings, the document is written in a form suitable for MITRE submission.
Petkit could, in principle, push a firmware update that defeats this integration. See FIRMWARE_PROTECTION.md for a layered defense (OTA-block, Internet firewall, don't-use-the-app, firmware-change detection automation). TL;DR: our server already refuses every OTA query, and blocking the feeder from the public Internet makes the setup virtually update-proof.
This integration modifies the behavior of your Petkit feeder in a way not sanctioned by Petkit. You do this at your own risk. The author is not affiliated with Petkit. Your warranty may be void if you touch the firmware or open the device. This integration itself does not modify firmware — it only changes where the feeder sends its HTTP traffic — but we can make no promises about Petkit's future firmware updates silently reverting things.
"Petkit" and "Fresh Element Solo" are trademarks of Petkit Network Technology Co., Ltd., used here for compatibility identification only.
PRs welcome for:
- Support for other D4-series feeders (YumShare, Dual-Hopper, etc.)
- Additional language translations (
translations/<lang>.json) - Improved schedule UI (custom Lovelace card?)
- Bug fixes and protocol improvements
Please:
- Keep the integration local-only. No cloud callbacks, no telemetry.
- Don't commit real MAC addresses, SSIDs, passwords, or firmware captures.
- If you add a new feature, update the README entities table.
Reverse engineering of the D4 cloud and BLE protocols, as well as the Home Assistant integration code, was done with substantial assistance from Claude (Anthropic). Hardware testing, captures, hypothesis validation, and all design decisions by Opcodeffm.
MIT — see LICENSE.




