Automating a Status Light With Home Assistant

The Story So Far

None of the links here are affialate links, they are just Amazon links for what I bought, no tracking, I don’t profit as all from them, they are just to make your life esier

The kids are home from school for the summer and my home “office” is a corner of my bedroom. As kids are want to do they often pop into my room to ask a question or just get some attention. This can be a problem as I am frequently in meetings. So I decided to go down on a journey and get a status light that would let everyone know if I was in a meeting and even what kind of meeting I was in as well as when Momma and I go to bed. I can assure you that this was a journey but I feel like I have finally arrived at the destination. This post isn’t to document that journey and all the frustations along the way but rather to give you my solution.

To be honest this is probably totally the wrong way to go about some of this stuff, I just poked stuff with proverbial sticks until I got it working like I wanted it to. I have no desire to create a repo and publish this for other people and less than zero desire to support any of this. I just want to show off my cool shit, so you get this blog post.

Caveats

  • This is how I did it, it is not necessarily the right way, it is only one way.
  • There is no implied warranty, promise of support, or even implication that following this guide is safe it may get your dog pregnant, cause your fridge to start freezing everything on the top shelf, make your toaster burn every other piece of bread, or blow up your washing machine and I am not responsible for any of that or anything else.
  • I am sure there are race conditions I haven’t seen yet and I may not update this later if I find them, consider this a starting point and nothing more.
  • I used yellow for meetings without video, red for meetings with video, and I use blue for when we go to bed in another automation. This information is next to useless, but here it is.
  • All the machines are Linux and that’s all I know how to work on.

Pre-requisites

  • Linux workstation running Pipewire for audio
  • Home Assistant running on HAOS (Docker isn’t enough)
  • I use an Elitedesk 705 as mini-server and HAOS runs on a QEMU virtual machine using a bridge interface so it has its own IP and acts, mostly, like a physical machine (I will post my full HA setup at some point but this was way cooler).
  • A Mosquitto broker running on HA
  • A light of some sort you can automate, I used: Portabe Smart Light (It’s not great and I will probably replace it but it works for now)

Workstation Triggers

I initially went down the rabbit hole of connecting my work calendar to HA, excluding certain things, and triggering based on calendar events. This was interesting but didn’t manage to capture ad-hoc meetings like Slack Huddles or a client sending me a quick invite link that never touched the calendar. So I nixed that and decided to try and find a polling solution. After a few days of trial and error I decided on the following triggers for work meetings:

  • Does PipeWire show an active mic source claimed by Chrome, Firefox, or Slack?
  • Is Zoom running?
  • Is the webcam device node in use by any of the processes in the same list as mic source?

The Workstation Scripts

These scripts run on the workstation and publish on/off to two MQTT topics. Home Assistant subscribes via auto-discovery and drives the light from those binary sensors.

/usr/local/bin/<workstation shortname>-meeting-poll

#!/usr/bin/env bash

MQTT_HOST="<HA IP>"
MQTT_USER="<your mqtt user>"
MQTT_PASS="<your mqtt password>"
DEVICE_ID="<workstation shortname>"
CHECK_INTERVAL=30

# --- Mic detection ---
mic_active() {
 pgrep -x zoom > /dev/null && return 0

 # PipeWire: any browser or Slack holding a mic source-output
 pactl list source-outputs 2>/dev/null \
 | grep -q 'application.process.binary = "\(chrome\|chromium\|firefox\|slack\)"' \
 && return 0

 return 1
}

# --- Camera detection ---
camera_active() {
 fuser /dev/video* > /dev/null 2>&1
}

# --- MQTT auto-discovery (run once on startup) ---
publish_discovery() {
 local base="homeassistant/binary_sensor/${DEVICE_ID}"

 mosquitto_pub -h "$MQTT_HOST" -u "$MQTT_USER" -P "$MQTT_PASS" \
 -t "${base}_meeting/config" -r -m '{
 "name": "In Meeting",
 "unique_id": "in_meeting",
 "state_topic": "homeassistant/<workstation shortname>/meeting",
 "device_class": "occupancy",
 "expire_after": 120,
 "device": {"identifiers": ["<workstation shortname>"], "name": "<workstation shortname>"}
 }'

 mosquitto_pub -h "$MQTT_HOST" -u "$MQTT_USER" -P "$MQTT_PASS" \
 -t "${base}_camera/config" -r -m '{
 "name": "<workstation shortname> On Camera",
 "unique_id": "<workstation shortname>_on_camera",
 "state_topic": "homeassistant/<workstation shortname>/camera",
 "device_class": "occupancy",
 "expire_after": 120,
 "device": {"identifiers": ["<workstation shortname>"], "name": "<workstation shortname>"}
 }'
}

publish_discovery

while true; do
 if mic_active; then
 MIC_STATE="on"
 else
 MIC_STATE="off"
 fi

 if camera_active; then
 CAM_STATE="on"
 else
 CAM_STATE="off"
 fi

 mosquitto_pub -h "$MQTT_HOST" -u "$MQTT_USER" -P "$MQTT_PASS" \
 -t "homeassistant/<workstation shortname>/meeting" -m "$MIC_STATE"

 mosquitto_pub -h "$MQTT_HOST" -u "$MQTT_USER" -P "$MQTT_PASS" \
 -t "homeassistant/<workstation shortname>/camera" -m "$CAM_STATE"

 sleep $CHECK_INTERVAL
done

/etc/systemd/system/<workstation shortname>-meeting-poll.service

[Unit]
Description=<workstation shortname> meeting state poller
After=network-online.target

[Service]
ExecStart=/usr/local/bin/<workstation shortname>-meeting-poll
Restart=always
RestartSec=10
User=<your username>
Environment=XDG_RUNTIME_DIR=/run/user/<your UID>

[Install]
WantedBy=multi-user.target

The XDG_RUNTIME_DIR line is required. Without it, pactl runs in a clean environment and returns no source-outputs — mic detection silently fails.

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable --now <workstation shortname>-meeting-poll

Home Assistant

MQTT broker

You need an MQTT broker reachable from both the workstation and HA. The Mosquitto add-on works fine.

  1. Install via Settings → Add-ons → Mosquitto broker,
  2. Create dedicated user for the workstation. The cleanest way is through the Mosquitto add-on’s own user management rather than HA user accounts:
    • Go to Settings → Add-ons → Mosquitto broker → Configuration
    • Under logins, add an entry:
logins:
  - username: <your mqtt username>
    password: <your mqtt password>
  1. Save and restart the add-on.

Alternatively, you can use a Home Assistant user and Mosquitto will accept those credentials automatically, but a dedicated entry in the add-on config keeps it cleaner and more secure.

Helpers

Create two input_boolean helpers. These can be added via the UI (Settings → Devices & Services → Helpers) or in configuration.yaml:

input_boolean:
  in_meeting_manual:
    name: In Meeting (Manual)
  status_light_meeting_controlled:
    name: Status Light Meeting Controlled
  • in_meeting_manual — manual override toggle (dashboard button type thing)
  • status_light_meeting_controlled — internal flag tracking whether the automation owns the light

Binary sensors

These are created automatically via MQTT auto-discovery when the script first runs. After a few seconds you should see:

  • binary_sensor.<workstation shortname>_in_meeting
  • binary_sensor.<workstation shortname>_on_camera

Both expire after 120 seconds if no message is received (heartbeat from the 30s poll loop keeps them alive).

Automations

Add these to automations.yaml:

Status Light — Meeting Color

This single automation handles both the initial color when a meeting starts and any camera changes during the meeting. Triggering on any change to either sensor and reading both states at execution time avoids race conditions.

- id: status_light_meeting_on
  alias: Status Light - Meeting Color
  triggers:
    - platform: state
      entity_id: binary_sensor.<workstation shortname>_in_meeting
      to: "on"
    - platform: state
      entity_id: binary_sensor.<workstation shortname>_on_camera
    - platform: state
      entity_id: input_boolean.in_meeting_manual
      to: "on"
  conditions:
    - condition: or
      conditions:
        - condition: state
          entity_id: binary_sensor.<workstation shortname>_in_meeting
          state: "on"
        - condition: state
          entity_id: input_boolean.in_meeting_manual
          state: "on"
  actions:
    - action: input_boolean.turn_on
      target:
        entity_id: input_boolean.status_light_meeting_controlled
    - choose:
        - conditions:
            - condition: state
              entity_id: binary_sensor.<workstation shortname>_on_camera
              state: "on"
          sequence:
            - action: light.turn_on
              target:
                entity_id: light.mgb_led_lights
              data:
                rgb_color: [255, 0, 0]
                brightness_pct: 40
      default:
        - action: light.turn_on
          target:
            entity_id: light.mgb_led_lights
          data:
            rgb_color: [255, 200, 0]
            brightness_pct: 40
  mode: restart

Status Light Off — Meeting Ended

- id: status_light_meeting_off
  alias: Status Light Off - Meeting Ended
  triggers:
    - platform: state
      entity_id: binary_sensor.<workstation shortname>_in_meeting
      to: "off"
    - platform: state
      entity_id: input_boolean.in_meeting_manual
      to: "off"
  conditions:
    - condition: state
      entity_id: input_boolean.status_light_meeting_controlled
      state: "on"
  actions:
    - delay: "00:00:30"
    - condition: template
      value_template: >
        {{ states('binary_sensor.<workstation shortname>_in_meeting') in ['off', 'unavailable'] }}        
    - condition: state
      entity_id: input_boolean.in_meeting_manual
      state: "off"
    - action: light.turn_off
      target:
        entity_id: light.mgb_led_lights
    - action: input_boolean.turn_off
      target:
        entity_id: input_boolean.status_light_meeting_controlled
  mode: restart

The 30-second delay before turning off prevents the light from blinking off between back-to-back meetings. The template condition also checks for unavailable — when the workstation is off, the MQTT sensor expires and goes unavailable, which should be treated as “not in a meeting.”


Random(ish) Thoughts

Why not use calendar? Calendar-based detection tells you when a meeting is scheduled, not if actually bothered to attend. This approach fires off when a mic or camera is claimed — whether it’s a scheduled call, an impromptu Slack huddle, or a browser tab with a web conference. It will fail if you are on a video call with no mic being attached but I don’t see a need for changing a status light in that instance, the manual toggle is enough for stupid video classed and stuff. A video call with the mic muted works just fine though.

pactl vs pw-cli: pactl works against PipeWire via its PulseAudio compatibility layer. It matches on application.process.binary (e.g. "chrome") — not application.name (e.g. "Google Chrome input"), which sane and not completely stupid.

Manual override: The input_boolean.in_meeting_manual helper lets you trigger the light manually. A dashboard toggle or physical button can flip it. I used this for testing but will be useful if someone drops a new meeeting app into my lap and I don’t have the bandwidth to update this in the moment.

Camera nonsense: The camera is only checked if a listed app is using the mic already, this is because I am lazy and wiring up everything to figure out what is using the camera was too much work and this metod worked for me.

Browsers: Another place I am being lazy, I only checked the browsers I am using or might use so YMMV if you use something I don’t.

 

No Gods, No Masters

There is no ethical consumption under capitalism


Fun with home assistant automation

By Wintermute, 2026-06-18