Inferno: a .NET smart smoker

A Raspberry Pi running .NET 8 replaces a pellet grill's stock controller, holds a set temperature with a PID loop, keeps the fire alive on its own, and plugs into Home Assistant over MQTT.

How it works

Flowchart: Home Assistant and a CLI reach the web API on the Raspberry Pi through an MQTT bridge or HTTP. The smoker mode loop runs a PID controller in Hold mode that sets seconds of auger feed per 20-second cycle, while a fire minder handles ignition, lid detection, and fail-safes. Relays drive the auger, blower, and igniter, and an RTD probe feeds temperature back.
The control loop in the middle, everything else hanging off the web API. Click for full size.

Inferno.Api runs on a Raspberry Pi inside the grill and is the only process that touches hardware. An RTD probe, read through an ADC, gives a smoothed grill temperature. In Hold mode a PID controller compares that reading to the setpoint and answers with a fraction, which becomes seconds of auger feed in each 20-second cycle. It works like cruise control, with pellets as the gas pedal, and it never feeds below a floor because a starved fire goes out. Beside that loop, a fire minder ticks every second. It lights the igniter on a cold start, tells an opened lid (a sudden cliff) from a dying fire (a slow slope), relights a struggling fire, and fails safe if the probe goes blind. Home Assistant, the CLI, and an MQTT bridge only change mode, setpoint, and P-value, through the web API.

Notable engineering details

  • PID with anti-windup. The integral is clamped so the I term alone never exceeds half a cycle, and the controller re-seeds after a sensor gap instead of computing a derivative across it.
  • Lid detection. A 30°F drop inside 10 seconds latches "lid open", runs a small fixed feed, and freezes the PID, so opening the lid doesn't light the igniter. Recovery starts only after 45 seconds below the check temperature.
  • Fail-safe paths. An out-of-range probe reads as NaN, never a stale value. Five more seconds of NaN during a cook forces Error, which cuts fuel and igniter and runs the blower. A clean shutdown de-energizes the relays.
  • Testable time. Time-based rules use an injected TimeProvider on a monotonic clock, so a clock jump on the RTC-less Pi can't trip a timeout. Tests drive the fire minder with a fake clock, and CI runs them on pushes and pull requests to main.
  • Home Assistant without YAML. The MQTT bridge publishes discovery configs, so entities and controls appear on their own, and it publishes retained state only on change.
  • Deployed with Pulumi. A version tag triggers a Pulumi C# program that deploys to the Pi over SSH.

Code and talks

Code walkthrough

Step through the code, chapter by chapter

Generated by code-walkthrough, a Claude Code skill I built. Every line number links to the code at a pinned commit, so you can check its work.

Back to the portfolio