New switch, fixed cameras, and a doorbell that actually rings
I picked up a used UniFi US-16-150W off eBay. Sixteen ports, 150 watts of PoE budget, managed through the same controller that already runs my access points. One dashboard for everything. Saved myself a few hundred dollars over buying new.
What I did not save was a weekend.
This is what happens when you stack three small infrastructure problems on top of each other and try to fix them in one sitting.
#The switch wouldn't adopt
Plug it in. Open the controller. Adopting... Seconds tick by. Offline. Adopting. Offline. Adopting. Offline. The kind of loop that means three things are wrong at once and any one of them would have been quick to find on its own.
Wrong inform URL. The switch came from a previous owner whose network resolved unifi as a hostname pointing at their controller. My network had no such DNS record. The switch was phoning home to a name that didn't resolve, getting nothing, and trying again forever. SSH in, manually set the inform URL to my controller's actual IP, problem one solved.
Wrong port. My UniFi controller runs in Docker. The inform port (8080 inside the container) was mapped to a different port on the host because of an old port conflict. The switch was trying 8080 against the host and getting nothing because the controller was listening on a non-default port. Found it via docker ps. Edited the compose file to map 8080:8080 since the original conflict was no longer relevant. Problem two solved.
Half-adopted state. Every previous failed adoption attempt had pushed credentials onto the switch that didn't match anything on either side. SSH started rejecting every password I tried. The fix is the universal "reset and start clean" — hold the reset button for ten seconds, wait for the boot, log in with the default ubnt/ubnt, and immediately set the correct inform URL before the controller can grab it on the wrong port again. The winning sequence was:
factory reset → wait for boot → ssh ubnt@<switch-ip>
→ syswrapper.sh set-adopt http://<controller-ip>:8080/inform
→ controller adopts cleanly
To prevent this for the next UniFi device, I added a DNS host override on the firewall: unifi → controller IP. Any UniFi device plugged into the network now auto-discovers the controller without intervention. That is the actual fix. The factory-reset dance was the recovery.
#The cameras forgot where they lived
After racking the new switch and replugging everything, all four cameras went dark in the camera management system.
Background: I'd preconfigured the cameras with their planned VLAN-30 addresses for an upcoming network migration. The migration hasn't happened yet. The cameras grabbed DHCP on the main LAN with completely different addresses than the management software expected.
Diagnosing it took longer than fixing it. The fix was three steps:
- Open the UniFi dashboard. Find the connected clients view, which lists every device with its MAC address.
- Match each camera's MAC to its entry. Note the new IP it got from DHCP.
- Update the camera management config with the new addresses.
The doorbell was the outlier — wireless rather than wired. It was still trying to connect to my old SSID, the one I'd retired a few weeks earlier when I rebuilt the WiFi config. I created the old SSID as a hidden network on the new APs temporarily. The doorbell reconnected. So did about a dozen IoT devices that had been silently attempting the old SSID since the switchover.
Set DHCP reservations on the firewall for all four cameras so the IPs stick. I did this via SSH directly to the firewall's CLI rather than clicking through the web UI — write_config() plus services_dhcpd_configure() from PHP, no clicking required. That is the part of pfSense I quietly love.
#The doorbell needed three separate fixes
Getting the doorbell into Apple Home through the camera management bridge was its own adventure. Three problems stacked, each with a different root cause, each clear-once-you-saw-it.
Problem one: mDNS confusion. The Docker host has two-dozen network interfaces — one real NIC plus all the Docker bridge networks for every running stack. The camera management container runs in host network mode, so the HomeKit plugin was trying to advertise the doorbell on every interface and getting confused by the bridge networks. The error was helpful in retrospect:
Reached illegal state! IPv4 address changed from undefined to defined!
The fix was inside the HomeKit plugin settings — change "mDNS Interfaces" from "Default" (which scanned everything) to "Server Address" (which advertises on only the real LAN). Save, restart, problem one gone.
Problem two: stale pairing. The doorbell had been paired to HomeKit before — back on the old network setup. Apple Home returned "accessory already in another home," which is the Apple-flavored version of "this device is configured to talk to a controller that no longer exists." Reset the HomeKit pairing in the camera management software's device settings. Re-add. Pairing succeeded. Problem two gone.
Problem three: device type. The doorbell was added to the camera management system as a Camera, not a Doorbell. HomeKit treated it as a camera — visible in Apple Home, streaming a feed, but with no ring notifications, no chime announcement on HomePods, no doorbell button on the iPhone notification. It was a camera with a button nobody ever noticed.
The fix was changing the device type in the management software. Removed from Apple Home, re-added as a doorbell. Now when someone presses the button, every iPhone in the house gets a notification with a snapshot, every HomePod announces it, and the camera feed comes up automatically.
That third fix is the one that turned a "working but broken" installation into "actually a doorbell." It's also the kind of thing that stays broken for years if nobody is paying attention to it, because the camera streams and the button doesn't do anything are both plausibly the intended state until you find out otherwise.
#The Uptime Kuma trick
While the hood was up, I cleaned up monitoring. Half the existing monitors were using localhost URLs, which doesn't work the way you'd think when Uptime Kuma itself runs in a container — localhost from inside a container is the container, not the Docker host. Every one of those monitors had been reporting itself green by accident because Uptime Kuma was checking its own port instead of the actual service.
Adding the new monitors needed for the switch, the access points, and the cameras was going to be twenty-something clicks through the web UI. I'd done that before. I didn't want to do it again.
Uptime Kuma's REST API is read-only. To create monitors programmatically, you have to talk to its Socket.IO interface — the same backend the web UI uses for everything write-side. There's no documented API for it, but the protocol is straightforward once you reverse-engineer it from the browser dev tools.
I wrote a Node.js script that connects to the Socket.IO endpoint, authenticates with username and password (API tokens don't work for write operations — that's a real limitation), and emits an add event for each monitor I want to create. One subtle gotcha that ate twenty minutes:
{
type: 'ping',
hostname: '10.x.x.x',
// accepted_statuscodes: ['200-299'] ← required even for ping monitors
}
Without accepted_statuscodes, the API throws "Cannot read properties of undefined," which is the kind of error message that exists to make you wonder if you're losing your mind. Ping monitors don't have status codes. The field is required anyway. Add it, the script works, problem solved.
The whole sequence took about two hours to write and now adds new monitors in roughly thirty seconds. The script and its protocol notes live in a runbook somewhere I'll definitely find next time, except for the half-hour I'll inevitably spend looking for it.
#The current state
The new switch is racked. The access points are managed. The four cameras are streaming. The doorbell rings — actually rings, in the "my iPhone gets a notification with a snapshot and the HomePods announce it" sense. Monitoring is honest now (not silently checking itself).
The VLAN migration is the next big move. The hardware is all in place. The configuration is scripted. It's a matter of scheduling the cutover for an evening when nobody at home will mind if the cameras blink offline for ten minutes.
Three lessons from this weekend that travel:
Stacked problems take three times as long to diagnose as the same three problems would have separately. Each one masks the next. The switch adoption fight wasn't a single bug — it was three small bugs lined up to look like one big mysterious problem. The fix for each was clear in isolation. Together, they presented as "adoption is broken." When something acts mysterious, suspect three small things, not one big thing.
The right fix is the one that prevents the next occurrence. The factory-reset dance to adopt the switch was real work. The DNS host override I added afterward — unifi → controller IP — is what prevents this same problem from happening on the next device. The recovery is not the fix. The fix is upstream of the next time.
The default state of "working but broken" is a real category of bug. The doorbell was paired, the feed was visible, the device showed up in Apple Home. Nothing was throwing errors. Nothing was generating tickets. And nobody could ring the doorbell. This kind of bug stays broken for years because it doesn't trigger anything — it just doesn't quite do its job. Watch for them.
That's a homelab weekend.