At three in the morning I asked what should have been a five-minute question: I have four access points in the house, what channel should each one be on? The controller said all four were on 2.4 GHz channel 6, with every 5 GHz radio at 160 MHz width. Seven hours later I had rebuilt the whole RF plan, and the two findings that carried the day had the same shape — the configuration screen was describing a network that did not exist.
The lock on a network that was switched off
I planned the new 2.4 GHz channels, went to apply them, and every change bounced. The UI said "This action could not be completed" and nothing else. No field flagged, no reason, no log entry. That sentence is the entire diagnostic surface the interface gives you.
Pushing the same change through the API returned the real error in one line: api.err.RadioChannelLockedByIot. There is a feature for IoT-heavy sites that pins every access point to one shared 2.4 GHz channel so that dumb devices never have to re-scan. It was switched on, inside one of my IoT SSIDs that was disabled at the time — and the lock applies site-wide whether or not the network it belongs to is broadcasting. A network that wasn't on had been rejecting every manual 2.4 GHz edit in the house. Which answered the original question too: nothing had gone wrong with auto-optimisation, and all four APs sat on channel 6 because something had explicitly told them to.
I fumbled the fix once — told the lock was the problem, I re-enabled the SSID and came back reporting the identical error, because enabling the network and clearing the channel lock are two different settings on the same page. There was a genuine scare in the middle of it too: for a few minutes nothing at all was broadcasting on 2.4 GHz, about 29 IoT devices with nowhere to associate, two bursts of roughly 25 disconnect events in four seconds each. That one was me, reshuffling SSIDs in the UI in parallel with the API work.
Same setting, twelve decibels apart
With the lock gone I dropped the 5 GHz radios from 160 MHz to 80 and split them across four channels, and then noticed something that had been true all along: the best AP in the house was carrying nothing. The Lounge unit is a 4x4 ceiling-mounted Pro Max with zero clients on 5 GHz, while two small in-wall units carried everything. All four radios were set to high transmit power, and yet the Lounge on channel 36 was radiating 17 dBm against the Office on channel 149 at 24.
My theory was the Australian regulatory domain, which caps the lower 5 GHz block far harder than the upper one. That's a theory, not a measurement, so I wrote the prediction down before testing it: swap the two APs' channels, and if the Lounge still reports 17 dBm on 149 then I was wrong and both channels go back.
After the swap, the Lounge read 29 dBm and the Office read 15. Same hardware, same high setting, everything identical except the channel number. Twelve decibels apart, about sixteen times the transmit power. It confirmed itself again when I moved the Office to channel 132 and it came up at 22 dBm instead of the 15 it managed on 36. The 36–48 block is now deliberately empty here.
I guessed the floor plan, and I guessed it wrong
Four APs across three non-overlapping 2.4 GHz channels means two of them have to share. I paired the Theatre and the Garage on channel 11, reasoning that they felt furthest apart. Felt. I had never seen the layout. Then I uploaded an actual floor plan with the AP positions marked: Theatre and Garage are on the same side of the building, about the third-worst pair available. The genuinely furthest-apart pair is Theatre and Office, diagonal corners of a single-storey house. Replanned in four minutes once I could see it.
The evidence was there before the plan arrived, and I'd flagged it at the time — the Garage's 2.4 GHz utilisation jumped from 61% to 75% right after the reshuffle, with a note beside it saying this is the pairing I guessed at without knowing the layout and it may be the wrong pair. An hour later it settled at 28%, so the spike was transient. The guess underneath it was still bad. Redone properly, the plan also dodged the two loudest things on channel 6: my dashcam at −56 dBm as heard by the Garage AP, which makes sense given the car parks under it, and a DIRECT- network at −51 from the Office that turned out to be the printer's Wi-Fi Direct radio.
The write that returned success and changed nothing
Late morning I tried to raise the minimum data rate on both 2.4 GHz SSIDs from 1 Mbps to 6, the standard trick for stopping slow chatty devices eating airtime. The call returned success: true and listed the field as updated. I ran it again to be sure. Same response. The stored value was still 1000 both times.
The blocker was a different field: the rate preference was set to auto, which makes the controller discard any manual rate handed to it. Trying to flip that in the same call failed too, because it sat outside the tool's field whitelist and got dropped without complaint. The only reason I caught it is that the response echoed the config back and I read it. This is the day that earned a standing rule in my notes — never trust a return code from this controller, re-read the stored value afterwards, every time. ok means the request parsed, not that anything happened.
The real fix was clicking it in the UI, under a menu path that wasn't the one I'd guessed at. The IoT network took 5.5 Mbps, the main one took 6. That gap matters more than it looks: 5.5 is an 802.11b rate and 6 is the lowest 802.11g one, so a floor of 6 makes 802.11b impossible on that network and removes the protection overhead b-compatibility charges to every transmission on it. A step change, not a 9% gain. The IoT network is still at 5.5, because it carries 29 devices I don't want to argue with.
What an hour of doing nothing proved
The plan was to change the rate, sit on my hands for an hour, then count devices and decide whether to roll back. Baseline: 33 wireless clients, 29 of them on the IoT network. After the hour, 29 of 29 still associated.
The airtime numbers were the payoff. Cleanest comparison is the Office AP, which didn't change 2.4 GHz channel across the window: utilisation went 40% to 17% while its client count went up, eight to nine. The Lounge halved, 20% to 10%, while doubling clients from six to thirteen. Office transmit retries went 16.7% to 3.0% — it had been re-sending roughly one packet in six.
Everything with a second radio moved itself, no band steering needed. The one I cared about was my phone, which had been clinging to a 6 GHz link from the far side of the house at −68 dBm and landed on the Office AP's new channel 132 at −38. That's the whole chain paying off at once: clear the hidden lock, find the power cap, move the Office somewhere it can actually radiate. The printer fixed itself too, bounced off the Garage AP it had been holding at −70 dBm from the far end of the building and back onto the Office at −53, in the room it had sat in the whole time. Not everything held — two Apple devices regressed when the SSIDs restarted, the phone among them, briefly preferring that 6 GHz link at −72 over the −38 a metre away.
The cameras, which I did not actually diagnose
Mid-morning my Protect cameras started dropping offline and reconnecting, and the obvious suspicion was that I'd caused it. I hadn't — all five are wired on the camera VLAN, and PoE was clean, the switch feeding them delivering 15.6 W of a 60 W budget with no alarms. What I landed on instead is a guess rather than a diagnosis: the gateway was reporting 88–90% memory and a one-minute load average of 3.90, and that single box routes, runs the network controller, and runs Protect for five cameras. Squeeze Protect and the cameras deregister while the network underneath stays perfectly healthy, which is what I was seeing. But my tooling reaches the network side only, so I couldn't read Protect's own logs, and a story that fits the evidence is not a root cause.
The annoying part is that I already had the answer. The previous evening I'd chased the same symptom from my phone and found it in Home Assistant's logs: the camera service on the gateway throwing 502s at the exact minutes all five went unavailable, which is the console's proxy answering while the thing behind it restarts. That was a different session in a different folder with its own memory, so this one started from scratch and reasoned its way to something vaguer. Two sessions, one symptom, no shared notebook. The fix for that isn't a better diagnosis, it's writing findings down somewhere both of them read.
The only firm thing I got out of the morning was unrelated: a firewall rule named Allow Cameras to Gateway that cannot fire, because it matches cameras by MAC with a source zone of Internal while every camera lives in Untrusted.
One address, eight rules, two meanings
That evening I bought a second-hand OptiPlex 3070 Micro to become a second Proxmox node, then measured the machine it is meant to help: 0.5 load average, 9.7 GB of 30 GB used. So it isn't more capacity, it's a second failure domain and a lab — a Home Assistant appliance VM, a second Uptime Kuma so an outage of the main server still alerts me, a second Cloudflare tunnel connector, and a staging container to rehearse updates on.
The evening went into writing the build plan rather than building anything, and the plan is where the interesting part was. Surveying the firewall first, I found x.x.0.5 sitting as a literal address in eight policies, meaning two different things depending on which rule you read. In five it means Home Assistant — how the IoT devices, cameras, Hue bridge and air conditioning all reach it. In others it means the server. Two are ambiguous. Nothing distinguishes them, because today Home Assistant is the server, so moving HA to its own address without first splitting those rules by intent breaks the Matter, Hue and air conditioning automations in a way that presents as an mDNS problem rather than a firewall one. A quieter one underneath: the rule letting the Home VLAN reach the server permits exactly two destinations, so the office PC on the management network reaches a new Proxmox UI fine while my phone, on Home, reaches neither that nor the new Home Assistant.
So the plan pre-allocates. Every guest gets a MAC assigned by hand rather than randomised by Proxmox, so it can be whitelisted and firewalled before it exists: everything additive goes in before the box is plugged in, both Home Assistant instances are permitted at once during the cutover so there's no window where neither works, and the old address comes out only after the new one is verified.
Nothing was executed. Every firewall call that night was a preview with confirmation deliberately withheld, and one got re-run twice — once for wrongly nested parameters, once because the proposed object omitted two fields that would have been silently re-defaulted on write. A no-op either way, but the object you send should be identical to the one already there apart from the additions.
The plan is written, the firewall changes are staged, and the machine they're for is still sitting on the desk running Windows. That's the correct order.