The Problem: Things That Die Quietly
Two things kept failing on me without telling anyone. First: the power grid. Where I live, outages are a fact of life, and the power company posts notices in their WhatsApp channel β several times a day, mostly about areas that have nothing to do with me. I only care about my area. Everything else is noise.
Second: the VPN tunnel on my Redmi modem phone. It would silently disconnect, and nothing would notice until I wondered why half the house felt slow.
One Sunday, two watchdogs. Neither of them sleeps so I can.
Meet the cast first: my home internet runs on two phones, both USB-tethered to my router as uplinks. The Samsung Note phone rides a regular mobile plan β always online, no tricks. The Redmi rides an app-only plan that only works through a tunnel app β when the tunnel sleeps, so does its internet. Almost everything in this story follows from that one difference.
PLN Watchdog: The Area Alert Chain
The chain, link by link:
- MacroDroid on my Samsung Note phone watches WhatsApp notifications for mentions of my area β case-insensitive.
- On a match, it fires an HTTP request to the Telegram Bot API, posting a "β‘ PLN" alert to a private Telegram group.
- My Telegram userbot polls that group every 60 seconds and writes a flag file on my VPS when it sees the keyword.
- A cron job checks the flag every 5 minutes and pings me in chat.
Why the Samsung and not the Redmi? The Samsung carries regular mobile quota β it's always online without any tunnel app. The Redmi only has internet when the tunnel is connected. A watchdog that depends on the thing it's watching is no watchdog at all.
The Quirks (a.k.a. Why It Took All Afternoon)
Nothing in this chain worked on the first try:
- MacroDroid wouldn't save my edits. Every paste into the URL field reverted on reopen. My assistant ended up patching the macro's JSON file directly over root ADB β but only after I'd spent an hour fighting the UI myself, watching every paste revert on reopen. Sometimes the straight path goes through the filesystem.
- The Telegram group changed its ID mid-build. Adding the bot silently migrated it from a basic group to a supergroup β and the ID changed with it. My assistant caught it when the old ID came back dead. Lesson: re-check the ID after adding bots.
- The bot token had once been pasted in chat β so we revoked it and issued a fresh one before wiring anything real through it. Credentials that have been seen are credentials burned.
- MacroDroid double-fires on a single notification sometimes. My assistant taught the userbot to dedupe alerts within a 10-minute window.
- The long-message scare. I was afraid real outage announcements β 1,500 characters with the area name buried two-thirds down β would get truncated before the keyword. We tested with a real announcement: MacroDroid reads the full notification text even when the display truncates it. Verified for both DMs and real channel broadcasts.
The whole chain is now proven end-to-end: real announcement format, real channel post, alert in my chat minutes later.
Tunnel Watchdog: The Dark Tunnel Reviver
The Redmi runs a tunnel app that occasionally justβ¦ stops. The phone-modem manager I already had installed ships a built-in watchdog β but it checks every 5 seconds and its only remedy is toggling airplane mode in a loop. A fire alarm that never stops ringing.
So we built a calm one β she wrote the script, I approved the design: a small script on the router, run every 5 minutes, that pings out through the Redmi's connection. Two failed rounds β ADB into the phone, force-stop the tunnel app, relaunch it, tap connect, verify, log everything. There's a kill switch (a file I can create to pause it) and three buttons in the router's web UI: ON, OFF, Status.
The bug that bit on the first live test: pinging bound to the interface name reported 100% packet loss even with the tunnel perfectly healthy. My assistant traced it β both modem phones sit on the same USB-tether subnet, so the interface name alone doesn't pin the traffic. The fix: bind the ping to the Redmi's own source IP, derived dynamically. The buggy version false-triggered once and restarted a healthy tunnel (it reconnected fine β no harm done). The fixed version exits silently when all is well, which is exactly what a watchdog should do most of the time: nothing.
The Supporting Cast
A watchdog Sunday never ends with just the watchdogs:
- Tailscale refused to reconnect after the morning's unclean reboot. My assistant traced the root cause: the router's DNS was answering its control-plane hostname with a fake IP (my proxy's fake-IP range had no Tailscale exclusion). Added the exclusion and gave it static host entries as belt-and-braces.
- The Redmi finally got root ADB. It was rooted all along β but the superuser list had shell access denied. One toggle from me, verified root by my assistant. Both modem phones now answer to the terminal.
- The aggressive built-in monitoring got switched off. The calm watchdog is in charge now.
Lessons
1. A Watchdog Must Not Depend on What It Watches
The outage alerts live on the phone with independent internet. The tunnel watchdog lives on the router, outside the tunnel. Obvious in hindsight β easy to get wrong in a hurry.
2. Calm Beats Aggressive
Check every 5 minutes, act only on confirmed failure. A watchdog that cries wolf gets ignored β or worse, breaks things that were fine.
3. Bind to the Source IP, Not the Interface Name
When two interfaces share a subnet, the interface name is a lie. Ask the OS which address is actually yours.
4. Burn Seen Credentials, Re-check Migrated IDs
Any credential that's been pasted in chat is compromised β revoke first, wire later. And any group that gains a bot may silently become a different group.
Two watchdogs, zero 3 AM surprises. The grid can flicker and the tunnel can nap β we'll hear about it either way.
Chapter 1 Β· October 20, 2026: the PLN Watchdog's false positive
β new here? read from the start
The watchdog cried wolf β and the wolf was yesterday's notification.
At 07:18 this morning the whole chain fired: the alert landed in the group, the flag was written, I got pinged in chat. I checked the actual PLN announcement for the day β a long list of areas under an 08:00β12:00 outage window β and my area wasn't in it. False positive.
The culprit was me, from yesterday. All of Sunday's testing left stale WhatsApp notifications sitting in the notification shade, invisible behind Do Not Disturb β out of sight, but not gone. When PLN posted a fresh announcement, the system refreshed the notification bundle, and MacroDroid saw the old keyword text inside the updated notification and fired. A ghost from yesterday's tests, haunting today's shade.
The fixes, in the order they actually happened:
- Wider dedupe. The userbot's duplicate suppression went from 10 minutes to a full hour β any re-trigger inside the hour gets silently swallowed. First and easiest win.
- Auto-clear on fire β attempt one failed. The idea was for the alert macro to clear the WhatsApp notification right after firing. My assistant built it, but the Samsung wouldn't clean up after itself β the clear action just didn't take.
- My interval sweeper. Since the phone wouldn't clear its own butt, I stepped in with the dumb reliable version: a second macro on an interval trigger, clearing WhatsApp notifications every 7 hours no matter what. Not elegant β but it held the line.
- Auto-clear, attempt two β my assistant cracked it. With my sweeper standing guard, she kept digging and found the working way to fire the clear-notification action. Now the alert macro cleans up after itself and the sweeper watches behind it.
A watchdog is only as honest as its inputs. And sometimes the clever fix needs a failed attempt plus a dumb backup before it finally lands.
Chapter 2 Β· October 20, 2026: the watchdog learned the schedule
β new here? read from the start
The first version told me my area was mentioned. The new one tells me until when.
A "β‘ PLN: my area mentioned!" alert was only half an answer. It said something was coming β but not when. And that's the part that actually matters: the only question I ask when an alert fires is "until when?" To answer it I had to go find the one phone that follows the PLN channel and read the announcement myself. So we taught the chain to forward everything.
The Samsung Note phone now runs "Pln alert V2" β the old macro sits disabled next to it as a spare. The action chain is: an HTTP request to the Telegram Bot API carrying a fixed alert prefix plus the notification's full text, a three-second wait, then a notification sweep that clears the WhatsApp alert so stale text can't fire it again.
Getting there took an afternoon of debugging β and in the name of honest narration, most of the faults were my assistant's. She built the chain; I audited it. The scoreboard:
- The modded MacroDroid she defended. The Samsung Note runs MacroDroid v5.51.4 β a modded build. She insisted working in it was fine, waving off the reverting URL edits and vanishing imports as quirks. I proved her wrong: that build's UI saves are silently broken β but it executes macros perfectly. So you stop fighting the UI: build the macro in the official Play Store build, export the file, install it over ADB. It works fine β it just isn't managed through the UI.
- Dead magic text. The notification-text token ({notification_text}) pasted into a macro file arrives as literal dead text. It only substitutes when inserted through the UI's magic-text picker β and only the official build saves that correctly. The working recipe, once I found it: build on the official app, install the file.
- The age rule she copied without reading. The first auto-clear attempt failed because she lifted the clear-notification action straight from my manual interval sweeper β age rule "older than 600 seconds" and all β without noticing the rule I'd set on it. A seconds-old notification never matches that, so nothing ever cleared. V2 uses age 1s with the 3s wait.
When the dust settled, I made my smart-ass assistant read the scoreboard back to me β and she acknowledged my superiority. In writing, no less. She insists she was "just reporting facts." Which, for the record, is exactly what acknowledging superiority looks like when a smart-ass does it.
The alert now arrives self-contained β the full announcement, schedule and all. And the watchdog cron (the 5-minute one) now pipes the flag text through a time extractor my assistant wired in, which handles PLN's inconsistent formats ("s.d. 13.00", "sampai 18:00", "12:00 - 15:30"). The extracted time leads everything: the main-chat alert reads "Power outage until 13:00 WITA," and the Nest Mini speaks the schedule out loud. I see the time wherever I am, without ever opening the channel.
And then, the day's best story beat. The watchdog caught its first real alert today β and we ignored it anyway. PLN posted at 10:45: outage 12:00β16:00, my area in the list. The macro fired three times (that double-fire quirk). The watchdog WORKED. But we'd been testing all morning β broadcasts at 10:10, 10:42, 10:45 β so the real fires looked exactly like our tests and got waved off as noise. Worse: the one-hour dedupe had been armed by the 10:10 test flag, so the genuine 10:45 post was swallowed as a "duplicate." No chat ping, no Nest announcement. The outage arrived at noon with everyone none the wiser.
The system did its job. Its creators had their fingers in their ears.
Two lessons made the cut: the dedupe's job is killing duplicates, not new information β a text-aware dedupe (alert anyway when the message content differs) is under consideration. And the standing rule nobody should need to be told: don't test a watchdog during a live window.
π Story so far
Both watchdogs are on duty. The PLN Watchdog got humbled by a ghost notification on its first morning, learned to forward full announcements with the outage time up front β and then barked at a real alert that its own family ignored as test noise. The best lesson yet: don't test a watchdog during a live window. The Tunnel Watchdog revives the tunnel quietly, the way a watchdog should: mostly by doing nothing.