A small Rust binary that turns a Philips WiZ smart bulb red whenever you're on a Microsoft Teams call. Mount it somewhere visible at home and your family stops wondering whether they can interrupt you.
No cloud. No Microsoft Graph API. No phone app middleman. Just a single binary on your laptop that watches Teams' own log file and speaks UDP directly to the bulb on your home network.
- Auto-detects Teams call status by tailing the local
MSTeams_*.logfile. No auth, no permissions, no Graph API. - Drives a Philips WiZ bulb over local UDP (port 38899). Discovery is automatic; you can also pin a known IP.
- Cross-platform — macOS, Windows, Linux. Single statically-linked binary, no runtime dependencies.
- Local web dashboard at
http://localhost:9876:- Live beacon that mirrors the bulb's actual color (read back via
getPilot) - Console with the live event stream
- Settings: bulb selection, work hours, grace period, color/brightness for call vs. idle states, manual override
- Live beacon that mirrors the bulb's actual color (read back via
- Configurable work hours so it doesn't trigger after-hours
- Grace period (default 10s) so back-to-back calls don't cause the bulb to flicker off-on-off
- Manual override — Force Red / Force Off / Auto buttons, useful for "I'm in a deep-work session, leave me alone"
- SQLite persistence — all settings + call history survive restart
- First-run convenience — if exactly one WiZ bulb is on your LAN at startup, it's auto-selected
Idle, no call:
Mid-call wrap-down (the spinner ring shows the grace timer counting down):
Download from the Releases page for your OS:
- macOS — Apple Silicon (
onair-aarch64-apple-darwin.tar.gz) or Intel (onair-x86_64-apple-darwin.tar.gz) - Windows —
onair-x86_64-pc-windows-msvc.zip - Linux —
onair-x86_64-unknown-linux-gnu.tar.gz
Extract and run. There are no dependencies — the binary is fully self-contained.
You'll need Rust (stable, 1.75+):
git clone https://github.com/dannotes/onair.git
cd onair
cargo build --release
./target/release/onairThe release binary is around 5–8 MB depending on platform.
- Run the binary —
onair(oronair.exeon Windows). - The dashboard opens in your default browser automatically (the first time only). If for some reason it doesn't, visit
http://localhost:9876. - If your WiZ bulb is on the same LAN and powered on, it gets auto-selected. Otherwise expand Settings → Bulb Selection → Scan Network and click your bulb.
- The bulb will turn red the next time Teams shows you as Busy or Do Not Disturb.
- Optional — make it run forever: expand Settings → Advanced and flip the Run on Login toggle. Onair will then start automatically on every login (you can close the terminal, reboot, and it just keeps working). Toggle off any time to disable.
That's it. The defaults are sensible and there's nothing else to configure unless you want to.
The first time onair runs, your OS may pop one or two privacy prompts. Click Allow on both — they're expected and onair only needs them for what it says on the tin:
- macOS Sequoia (15.x): "onair would like to access data from other apps"
Triggered because the Teams log lives in Microsoft's Group Container directory under
~/Library/. Onair only reads the file; it never writes to Teams' container. - macOS Sonoma+ (14.4+): "onair would like to find and connect to devices on your local network"
Triggered when onair sends UDP discovery /
getPilotto the bulb on your LAN. Required for any LAN bulb control. If you miss this prompt, broadcast and unicast UDP get silently dropped — symptom is "0 bulbs found" while the bulb is clearly online (you canpingit but onair can't reach it). Re-runonair, watch for the prompt, click Allow. Or open System Settings → Privacy & Security → Local Network and toggle onair on manually. - Windows / Linux: no first-run prompts. Just runs.
The grant is per-binary, so if you uninstall onair and reinstall it, you'll get the prompts again.
Open the dashboard, expand Settings → Advanced, and flip the Run on Login toggle. Onair gets installed as the OS-native autostart entry — a launchd LaunchAgent on macOS, a systemd user unit on Linux, or a Startup-folder shortcut on Windows. Toggle off any time to remove it cleanly. No manual file editing required, ever, and disabled by default so installing onair never modifies your login items without your explicit consent.
┌─────────────────────┐ ┌──────────────┐
│ your laptop │ ─── UDP 38899 ──────► │ WiZ Bulb │
│ │ │ (on the LAN)│
│ onair binary │ ◄── getPilot reply ── └──────────────┘
│ │
│ - tails Teams log │ HTTP
│ - drives the bulb │ ◄──────────────────── your browser
│ - serves dashboard │ localhost:9876
│ - SQLite config │
└─────────────────────┘
The state machine:
busy / dnd
┌──────────────┐ ──────────────────────► ┌──────────────┐
│ available │ │ on a call │
│ bulb off │ ◄─────────────────────── │ bulb red │
└──────────────┘ available + grace 10s └──────────────┘
▲ │
│ │
└────────── if back to busy within ──────────┘
grace, stay red
(back-to-back calls)
A small monitor task polls the latest MSTeams_*.log every 3 seconds (configurable), reads only new bytes since the last poll, and watches for presence transitions. Independently, a second task polls the bulb's getPilot every 5 seconds so the dashboard beacon shows the bulb's actual color, not a guess.
When Teams flips Busy → Available, a 10 second grace timer starts. If you go busy again within that window, the bulb stays red. Otherwise the timer expires and the bulb turns off (or whatever you've configured for "When Not on a Call").
Everything is editable from the dashboard's Settings panel. There's no config file you need to touch by hand.
| Setting | Default | What it does |
|---|---|---|
| Work Start / End | 06 / 18 | Hour-of-day window. Outside these hours new presence transitions are ignored, but an active call keeps the bulb red until it actually ends. |
| Poll Interval | 3 s | How often to check the Teams log for new presence events |
| Grace Period | 10 s | How long to wait after busy → available before turning the bulb off |
| Max Call Cap | 4 hours | Safety cap. If the bulb stays red for this long (e.g. Teams crashed), revert to idle. |
| Teams Offline | 5 min | If the Teams log hasn't been written to for this long, treat Teams as offline and turn the bulb off |
| When On a Call | red @ 100% | Color and brightness — or off, if you want an inverted setup |
| When Not on a Call | off | Color/brightness or off. Set to a warm white if you want an always-on ambient lamp. |
| UI Port | 9876 | Dashboard port (restart required) |
Config and call history are stored in a small SQLite database at:
- macOS:
~/Library/Application Support/Onair/onair.db - Windows:
%APPDATA%\Onair\onair.db - Linux:
~/.config/onair/onair.db
You can delete this file at any time — the next run will recreate it with defaults and re-detect your bulb.
| OS | Status | Default Teams log path |
|---|---|---|
| macOS | Verified | ~/Library/Group Containers/UBF8T346G9.com.microsoft.teams/Library/Application Support/Logs/ |
| Windows | Believed correct (per Microsoft's spec for the new Teams) | %LOCALAPPDATA%\Packages\MSTeams_8wekyb3d8bbwe\LocalCache\Microsoft\MSTeams\Logs\ |
| Linux | Best-effort guess | ~/.config/Microsoft/Microsoft Teams/logs/ |
Teams logs the user's status differently per platform:
- Windows writes lines like
... TaskbarBadgeServiceLegacy: ... GlyphBadge{"busy"} ... - macOS writes lines like
... UserDataCrossCloudModule: CloudStateChanged: ... { availability: Busy, ... }
onair has a separate regex per OS that normalizes both to the same internal Presence enum.
If your Teams installs the log somewhere else, open Settings, paste the path into Teams Log Path, click Verify, and onair will check that the directory contains real log files and confirm the parser can find a presence event in them.
Drop-in templates for all three OSes live in dist/autostart/. Pick your platform and follow the README in that folder — it's a 30-second copy-paste:
| OS | Mechanism | File |
|---|---|---|
| macOS | launchd LaunchAgent | dist/autostart/macos/com.dannotes.onair.plist |
| Linux | systemd user unit | dist/autostart/linux/onair.service |
| Windows | Startup folder shortcut (PowerShell installer) | dist/autostart/windows/install-startup.ps1 |
Quick start (macOS, assuming you installed via Homebrew):
cp dist/autostart/macos/com.dannotes.onair.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.dannotes.onair.plistQuick start (Linux):
mkdir -p ~/.config/systemd/user
cp dist/autostart/linux/onair.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now onairQuick start (Windows, in PowerShell):
.\dist\autostart\windows\install-startup.ps1See dist/autostart/README.md for full instructions, log locations, and how to uninstall.
The dashboard is just a thin client on top of a small JSON API. You can hit it directly with curl:
| Method | Path | Purpose |
|---|---|---|
GET |
/ |
the embedded dashboard HTML |
GET |
/api/status |
full live snapshot — presence, bulb state, override, stats |
GET |
/api/logs |
event ring buffer (?limit=&offset=) |
GET |
/api/config |
current config |
POST |
/api/config |
partial config update |
POST |
/api/override |
{"mode": "auto" | "force_red" | "force_off"} |
POST |
/api/discover |
trigger LAN bulb discovery |
POST |
/api/bulb/select |
{"mac": "...", "ip": "optional"} |
GET |
/api/bulb/state |
cached getPilot snapshot |
POST |
/api/bulb/test |
{"mode": "call" | "idle"} — flash bulb in that mode for 3 s |
POST |
/api/teams/verify |
verify a Teams log directory works |
GET |
/api/calls |
call history (?days=) |
- Rust 2021, async with Tokio
- Axum for the web layer
- rusqlite (bundled) for persistence
- Vanilla HTML + CSS + JS for the dashboard — no framework, no build step. Embedded in the binary via
include_str!.
The whole project compiles to a single binary with zero runtime dependencies.
- Fonts: DM Sans (headings) and JetBrains Mono (everything else), via Google Fonts.
- The WiZ UDP protocol has been documented by various community efforts; this implementation works against off-the-shelf WiZ bulbs with no firmware modification.
MIT — do whatever you want with it.
Issues and PRs welcome. Two things would help most right now:
- Verify the Windows path +
GlyphBadgeregex against a real Windows Teams installation. The defaults are believed-correct from the Microsoft spec but unverified on real hardware. - Verify or update the Linux path — Teams on Linux is uncommon and may have moved.
If your default doesn't work, the manual path override + Verify button in Settings is the workaround in the meantime.

