A few weeks ago I added a UPS and backup internet to my homelab. That solved one class of problem, but it made another one more obvious.
My UniFi gear knew when the WAN failed over. It knew when the UPS switched to battery. Kubernetes did not.
So if the main WAN failed at 3 AM, qBittorrent could keep happily seeding over a metered backup link. If power dropped, the UPS could be counting down its remaining runtime while the cluster continued doing background ML jobs, backups, and other work that absolutely did not need to happen during an outage.
That felt wrong. The network already had the state. The cluster just needed to react to it.
That is why I built UniFi Reactor: a Kubernetes operator that watches UniFi Network state and turns it into declarative cluster automations.
Docs are here: reactor.robbeverhelst.com
UniFi Reactor polls the UniFi Network API, normalizes what it sees into state keys, and reconciles Kubernetes Automation
resources against those keys.
Examples of state keys:
wan: primary
or wan: backup
internet: ok
, degraded
, or down
ups: online
or on-battery
ups.battery: normal
, low
, or critical
devices: all-online
or degraded
device.<name>: online
or offline
Then you write rules like this:
apiVersion: reactor.robbeverhelst.com/v1alpha1
kind: Automation
metadata:
name: shed-ml-on-battery
namespace: reactor-system
spec:
when:
provider: unifi
state:
ups: on-battery
actions:
- type: kubernetes.scale
target:
kind: Deployment
name: immich-machine-learning
namespace: immich
replicas: 0
onExit:
- type: kubernetes.scale
target:
kind: Deployment
name: immich-machine-learning
namespace: immich
replicas: 1
When the UPS goes on battery, Reactor scales down Immich machine learning. When mains power returns, it scales it back up.
That was the first real automation I deployed because it is boring in exactly the right way. Photo indexing is useful, but nobody cares if it s during a power cut.
I already have Prometheus, Grafana, Gatus, and the usual homelab observability stack. They are good at telling me something happened.
But an alert that says "the UPS is on battery" is only half useful. The question I actually care about is: what should the system do while that condition is true?
That difference shaped the project:
For simple notification-only cases, Prometheus is probably still the better tool. Reactor becomes interesting when the response is operational: downloads, suspend CronJobs, scale down optional workloads, disable guest WiFi, or shed PoE load.
The first design choice was to make Reactor state-driven instead of event-driven.
That means polling is the source of truth. Webhooks can be a fast path later, but they should not be the mechanism of record.
Why? Because one-shot events are easy to miss. The controller can restart. The network can flap. A webhook can fail. If the system only reacts to edges, it can get stranded in the wrong mode.
With state reconciliation, the next observation corrects things.
The mental model is closer to Kubernetes itself: observe reality, compare it to desired state, reconcile.
A simplified flow looks like this:
UniFi Network API
|
v
UniFi provider observes hardware state
|
v
Normalized keys: wan, ups, internet, devices, ...
|
v
Reactor matches Automation resources
|
v
Actions are applied while conditions hold
The flagship use case is metered backup internet.
I run a UniFi setup with a primary WAN and backup connectivity. Once failover is verified end-to-end, I want this kind of automation:
apiVersion: reactor.robbeverhelst.com/v1alpha1
kind: Automation
metadata:
name: -downloads-on-backup-wan
namespace: media
spec:
when:
provider: unifi
state:
wan: backup
actions:
- type: kubernetes.scale
target:
kind: Deployment
name: qbittorrent
namespace: servarr
replicas: 0
onExit:
- type: kubernetes.scale
target:
kind: Deployment
name: qbittorrent
namespace: servarr
replicas: 1
When the network is on backup WAN, downloads stop. When primary WAN returns, they resume.
That is a tiny YAML file, but it encodes a very practical policy: do not burn backup data on background traffic.
Other examples I want to add around this:
Some of those are not implemented yet, but the model is clear.
Power loss is the other obvious axis.
A UPS buys time. The cluster should spend that time intelligently.
The first deployed reaction was:
ups: on-battery
-> scale immich-machine-learning
from 1 to 0Future power-loss automations can become more aggressive as the battery drains:
ups: on-battery
-> suspend non-critical CronJobsups.battery: low
-> scale down heavier optional workloadsups.battery: critical
-> shut down or isolate more aggressivelyOne important detail: ups
and ups.battery
are separate keys.
That is deliberate. If a single enum went from online
to on-battery
to battery-low
to critical
, an automation matching on-battery
would stop matching when the battery became low. That could accidentally fire onExit
and scale workloads back up during the outage.
Separate keys avoid that. An automation can match ups: on-battery
for the whole outage, while another automation can additionally match ups.battery: critical
for escalation.
A surprisingly important part of the design is onExit
.
Reactor does not guess how to undo something. The automation says what should happen when the condition starts holding, and what should happen when it stops.
actions:
- type: kubernetes.scale
target:
kind: Deployment
name: qbittorrent
namespace: servarr
replicas: 0
onExit:
- type: kubernetes.scale
target:
kind: Deployment
name: qbittorrent
namespace: servarr
replicas: 1
That is more verbose than magic, but it is safer. Infrastructure automation gets scary when the tool assumes what "back to normal" means.
This was the part that made the project more than a wrapper around a few scripts.
Imagine qBittorrent should when:
Those are independent reasons. Either one should keep qBittorrent down.
If one automation exits while the other still matches, Reactor must not scale the deployment back up.
So Reactor arbitrates shared targets. Desired-state actions like kubernetes.scale
are treated as levels. The most restrictive active level wins. A workload comes back only when no matching automation still wants it down.
That avoids the classic failure mode where two scripts fight each other:
Reactor tracks the active claims instead.
I wanted this to be safe enough to run in my actual cluster, not just impressive in a README.
The operator uses:
cluster-admin
For example, actions like HTTP requests, UniFi WLAN changes, PoE cycling, and UPS outlet control are refused unless the destination or target is explicitly allowed.
That matters because a Kubernetes operator with access to your network controller is a sharp tool. It should not become "curl with cluster permissions".
The project already supports more than just scaling Deployments.
Action families include:
kubernetes.scale
kubernetes.cronjob.suspend
kubernetes.cordon
kubernetes.restart
http.request
notification.ntfy
, notification.discord
, notification.slack
homeassistant.service
qbittorrent.
and qbittorrent.resume
unifi.wlan.enable
and unifi.wlan.disable
unifi.poe.cycle
unifi.outlet.cut
and unifi.outlet.restore
There is intentionally no arbitrary shell action. That would be convenient, and also how you turn a small operator into a haunted CI runner.
A few lessons stood out.
First, UniFi exposes useful operational state, but it is not always shaped the way you would design a clean public API. Normalizing it into a small vocabulary is worth the effort.
Second, "event-driven" sounds elegant until you care about recovery. For infrastructure state, reconciliation beats clever edge handling.
Third, undo behavior deserves first-class design. It is easy to write automation that does something. It is harder to make it stop doing that thing at the right time.
Fourth, homelab projects are best when they are boring under pressure. A power cut is not the moment to discover your automation has opinions.
The project is public and released as a multi-arch container image and Helm chart.
v1.2.0
The next things I want to harden are:
The broader idea is simple: your infrastructure already knows a lot about the conditions it is running under. UniFi Reactor is my attempt to let Kubernetes respond to that context without turning the homelab into a pile of one-off scripts.
That is the kind of boring automation I want more of.