One button is all it takes – feed the fish, please
Anyone with a garden pond and fish who feeds them from time to time knows the problem: the moment the food hits the water, the skimmer sucks in the pellets or flakes before the fish even reach the surface. The food ends up in the filter, the fish stay hungry, and the water gets an extra dose of nutrients you really didn't want.
The solution is as old as the pond pump itself: just switch it off briefly, feed the fish, switch it back on a few minutes later. The only catch is that the switch by the pond isn't always within reach, that you forget to turn it back on at the end of the day, and that your hands are dirty on the way to the food container anyway.
That's exactly what the Pond Skimmer is for: a button in a small web app that pauses the pump's socket while the food is floating, and turns it back on after a configurable time (30 minutes by default). Tap, feed, walk away.
What the app does
The concept is deliberately minimal: one-button operation, no login, no configuration in the UI.
- "Disable skimmer" writes a timestamp into a small lock file inside a Docker volume. From that moment on the pump is off – no matter who tries to turn it back on.
- A second service, the sync loop, reads this file every second and enforces the state against Home Assistant. Whoever flips the socket in the meantime – the phone, an automation in HA, the wireless switch by the pond – the loop will switch it off again if in doubt.
- Once the pause expires (30 minutes is usually more than enough for feeding) it turns the socket back on automatically.
That is last-writer-wins in its purest form: the app is the last instance to say "don't pump right now." Even if the app is closed in the meantime, the state lives on inside the container, and the food has time to sink to the bottom before the fish find it.
Architecture: two containers, one truth
Web app (smartphone, patio, sofa)
| POST action=toggle / status
v
docker-compose: web + sync
web ----> share/skimmer.lock
sync <---- (reads every second, writes against HA)
| REST API
v
Home Assistant (switch.skimmer_socket_1)
The trick: the app never talks to Home Assistant directly. It doesn't even know HA exists – it just writes a file. The sync service is the only component that knows an HA token. This keeps the web container's surface area clean and the attack surface small.
The two containers at a glance:
web— Static PHP front-end app (PWA) +ajax.php. Holds no HA token (apart from thePAUSE_MINUTESconstant).sync— PHP loop running on a 1-second cycle against Home Assistant. Holds the HA token (HA_URL,HA_TOKEN).
A setup like this fits on any Linux server, any NAS, or any container cloud provider – the only requirement is Docker Compose and a reachable Home Assistant on the network.
Pause logic in detail
The lock file share/skimmer.lock contains the Unix timestamp of the last deactivation. Every second the sync service decides based on these rules:
- Pause active, socket on →
turn_off(pause is enforced). - Pause active, socket off → nothing.
- Pause not active, socket off →
turn_on(pause ended / target state active). - Pause not active, socket on → nothing.
"Pause active" means: timestamp >= now - PAUSE_MINUTES. If the socket gets switched back on during the pause by another automation, the loop switches it off again within a second. That gives you robust last-writer-wins behaviour without any locking tricks – and the food is guaranteed time to sink in peace.
The app itself shows the status with a small poll bar at the bottom: green = last response less than 5 minutes ago, yellow = less than 15 minutes, red = older or errored. The colour is updated by a 10-second timer even when no new poll comes in – handy for seeing the status even if the app has been in the background for a while.
PWA with a real offline view
A PWA without offline support is just a bookmark on the home screen. That's why the current release ships a thoughtful offline experience that doesn't just show a generic "no connection" toast:
- Service worker caches the app shell (HTML, CSS, JS, manifest) on first launch. GET requests then run network-first and only fall back to the cache when the network really is gone. Important: only successful responses (
response.ok) are cached – a broken 500 page doesn't end up in the offline treasure. - Last known state is written to
localStorageafter every successful status poll. - When the device goes offline (patio with weak Wi-Fi, reverse proxy briefly hanging), the app shows no empty screen but puts a banner at the top: "Offline – no access to the pond. Last state from HH:MM." The toggle button stays visible but locked.
- As soon as the browser fires the
onlineevent again, the banner disappears, the controls unlock, and an immediate status poll fetches the current state.
In concrete terms: you can sit on the patio and feed the fish without worrying about Wi-Fi. Even if the app can't reach the server right now, it still knows the skimmer is paused until 14:30 – and it blocks silly clicks that would have led nowhere anyway.
Deployment in under five minutes
git clone https://github.com/jschwind/teich-skimmer.git
cd teich-skimmer
cp .env.example .env
# Fill in HA_URL, HA_TOKEN, SKIMMER_ENTITY_ID
docker compose up -d --build
The app then listens on http://<host>:8080/. For PWA installation on the phone you need HTTPS – the most common solution is a reverse proxy in front of the stack (Caddy, Traefik, Nginx, Cloudflare tunnel). On your own LAN, Tailscale with HTTPS works just as well.
A note on security: The app intentionally has no login. Anyone who knows the URL can pause the skimmer. On a private network that's deliberate; if you expose the stack publicly, you should put an auth layer in front of the reverse proxy (basic auth, mTLS, Tailscale access controls, etc.).
What's in the repo
teich-skimmer/
├── docker-compose.yml # web + sync services
├── Dockerfile # slim PHP image
├── README.md # setup, screenshots, architecture, troubleshooting
├── app/
│ ├── public/ # PWA (HTML, CSS, JS, manifest, service worker)
│ ├── lib/ha.php # HA REST helper (curl, 5/10 s timeouts)
│ └── sync.php # 1-second sync loop
└── docs/
├── architektur.md # architecture diagram + sync table
└── *.jpeg # screenshots from the app
Licence: MIT. The repo is meant to be a useful starting template for anyone who needs something similar – a switchable socket, simple pause logic, a nice frontend, an API-only bridge to HA.
Three things I did differently on the second pass
- HA token only where it's needed. Earlier versions had the long-lived token in the front-end container – today it lives exclusively in the
syncservice. Saves an unnecessary copy of the token in a container and reduces the attack surface. - Bump the cache version explicitly together.
$vinindex.phpandCACHE_NAMEinsw.jsmust always be bumped together. Anyone who forgets ends up with users stuck on an old app shell on their phone for an hour, wondering why the new buttons are missing. - Make a conscious call about telemetry. Every open app reports a heartbeat on poll (time, visibility, screen, timezone, IP) to
share/heartbeat.json. That's handy for diagnostics, but it should be mentioned in the README – anyone who doesn't want it can rip the block out ofajax.php.
Conclusion
The Pond Skimmer is a good example of a "small" tool that still deserves a few architectural decisions: API-only instead of a custom component, lock file as the single source of truth, container instead of a cron job on the Pi, a real offline view instead of just a pretty manifest. All of that fits in a repo with three PHP files and ~600 lines of code – and still feels like a real app.
If you'd like to adapt the setup to your own socket, you'll find the repo at github.com/jschwind/teich-skimmer . Pull requests for similar "switch-something-off-for-X-minutes" problems are explicitly welcome – fans, light zones, irrigation, anything you want to switch off briefly and then forget about.
Interested? Let's work together
Got a similar challenge, want to build your own little control stack, or need help connecting your devices to Home Assistant? Then get in touch:
- Email: web@juergen-schwind.de
- WhatsApp: +49 179 22 11 00 5
Let's work together.
This post describes the standalone stack of the Pond Skimmer as a pure example application. The architecture (lock file + API sync loop + PWA) can be transferred to many similar "switch-something-off-for-X-minutes" problems – pumps, fans, light zones, irrigation.