Home A-CI-stant

English
, , ,

Hi!

For years I lurked in the Home Assistant channel of a community Discord, but never had much interest in it. Earlier this month I had some discount coupons in hand and decided that it was time to give it a go. I got Home Assistant, the ZBT-1 and one IKEA Inspelning and managed to make it control my balcony lights. In my opinion, the sun “device” in Home Assistant might be the most underrated out-of-the-box integration!

In any case, I live in an apartment, so after that I was mostly done. I don’t have much else to automate, and I do really value physical buttons, so it all sat there for a while. This weekend, while doing some maintenance on my NAS, it got me thinking: what if I use Home Assistant to manage a Continuous Integration server?

I run my own Forgejo instance, and I’ve been meaning to use the Actions feature for a while, but never got around it in my NAS because I don’t want to deal with VMs nor Docker-in-Docker. I also have a fairly decent Optiplex laying around, and while I already had it running a Forgejo Actions runner before, I simply have way to little usage of it to justify have it running 24x7.

So now it finally clicked. The Optiplex has a feature to ATX-power-on the motherboard once it is energized, and combining that with the smart plug, I can power it on just when there is demand. With that, I made a daemon that will wait for Forgejo’s webhooks, and turn on the CI if needed:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
m.HandleFunc("POST /webhook", func(w http.ResponseWriter, r *http.Request) {
    // ...

    if payload.Repository.FullName != "canastra.online/monorepo" {
        w.WriteHeader(http.StatusNoContent)
        return
    }

    version, ok := strings.CutPrefix(payload.Ref, "refs/tags/")
    if !ok {
        w.WriteHeader(http.StatusNoContent)
        return
    }

    isOn, err := hassIsOn(hassEntityId)
    // ..

    if isOn {
        w.WriteHeader(http.StatusNoContent)
        return
    }

    err = hassTurnOn(hassEntityId)
    // ...

    log.Printf("Waking up CI server for %s:%s\n", payload.Repository.FullName, version)
    w.WriteHeader(http.StatusNoContent)
})

However, that only addresses half of the issue. Once the server is on, it must be powered off in case it’s not needed anymore. A systemd timer can take care of this, powering off the server 30 minutes after boot. We can abuse the runner’s shutdown_timeout to make systemd wait for the running jobs to complete.

1
2
3
4
5
6
7
8
9
10
11
12
# forgejo-runner.service
[Service]
TimeoutStopSec=30m # same as your runner's shutdown_timeout

# hass-ci.service
[Service]
ExecStart=systemctl stop forgejo-runner.service && systemctl poweroff
TimeoutStopSec=30m

# hass-ci.timer
[Timer]
OnBootSec=30m

Check out the complete code, MIT licensed.

The last piece of the puzzle is a Home Assistant automation that will turn off the plug once the server is completely off.

home assistant automation to turn off the plug when not used

And that’s about it. Granted, it’s totally unnecessary, I could’ve just used Wake-Up on LAN, but it was the perfect project for me to dig into Home Assistant and tell myself I need just one more smart plug.

Thank you!