A real-world "on a meeting" light sign
![A real-world "on a meeting" light sign](/a-real-world-on-a-meeting-light-sign/IMG_20240518_172824_hu3716552850767094554.jpg)
Me and my wife work remotely and we do it in the same room, but there is a problem: the meetings, and obviously, working remotely we have, let’s say, more than we would like, and is not always easy to know when the other is in a meeting.
I decided to find a solution to make it visible when someone is in a meeting simply, only with a sight.
The solution: Put a light sign on the working room’s wall and turn it on when I am in a meet and off when I am not, simple. :)
The light sign
I found a cheap neon-like sign on an online store and I order it. This light sign is powered via USB and controlled manually with a switch button, so that I could control it automatically. The neon-like is just a shaped LED strip, so I ordered a wifi led controler (and a power supply), to replace the light sign’s controller, and now we can control the light via wifi using Tuya’s app.
We solved the hardware side, now we must solve how to detect when I am in a meet
Detecting when I am in a meet
I guess there are a lot of ways to detect when you are in meeting, but I decided to use the webcam, then the solution works with any meet application: Zoom, Google meet, discord, slack, etc
Doing a simple search I found that on Linux (if the kernel uses modules, which is common) you can execute:
$ lsmod | grep uvcvideo
And get something like:
uvcvideo 139264 0
videobuf2_vmalloc 20480 1 uvcvideo
uvc 12288 1 uvcvideo
The uvcvideo
module is the one we are interested on (the first line), in that line, the first number means the module
size in bytes (we can ignore it) and the second means the number of instances of the module in use, in our case: 0
means webcam not in use 1
implies webcam in use.
Then we can create an script or a program to check this value and turn on or off the wifi controller, but before seeing the code:
Using Home Assistant to control the light
I am a big fan of Home Assistant, as is a very flexible way of managing home automation using devices of multiple brands. This is the reason why I decided to use it, instead of fighting with the wifi controller maker’s API (if available), I will delegate that to Home Assistant, as you can find integrations for a lot of makers standardizing the way we control the light
The next thing I did was to create a virtual switch in Home Assistant, this will allow me to store the webcam’s usage status in Home Assistant and then trigger a scene to turn on or off the light.
Go to
Settings
>Devices & services
>Helpers
tab > Click onCreate helper
> Selecttoogle
Doing that instead of changing the light status directly, allows us to be more flexible and create better automation, for example to also turn on another light or pause the vacuum cleaner when you are in a meetings to reduce ambient noise.
After that we need to create an access token for the Home assistant’s API
Click over your username >
Security
tab > Click onCreate token
Those are the automation I create:
alias: Turn On meeting light
description: ""
trigger:
- platform: state
entity_id:
- input_boolean.on_a_meet
to: "on"
for:
hours: 0
minutes: 0
seconds: 6 # I added a minimum time to avoid false positives
condition: [ ]
action:
- type: turn_on
device_id: #the light device id
entity_id: #the light entity id
domain: light
brightness_pct: 11
mode: single
alias: Turn Off meet light
description: ""
trigger:
- platform: state
entity_id:
- input_boolean.on_a_meet
to: "off"
condition: [ ]
action:
- type: turn_off
device_id: #the light device id
entity_id: #the light entity id
domain: light
mode: single
With that, we can create a program to check the webcam’s status and send it to Home Assistant
The observer
I created a program written in Go to check the uvcvideo
module status and send the changes to Home Assistant
package main
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"log"
"log/syslog"
"net/http"
"os"
"strconv"
"strings"
"time"
)
var prevState *bool // nil = unknown, true = active, false = inactive
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
func logError(e error) {
if e != nil {
fmt.Println(e.Error())
log.Default().Println(e.Error())
}
}
func updateHassStatus(ctx context.Context, status bool) (err error) {
host := getEnv("ON_A_MEET_HASS_SERVER", "http://192.168.0.104:8123")
token := os.Getenv("ON_A_MEET_HASS_TOKEN")
entityID := getEnv("ON_A_MEET_ENTITY_ID", "input_boolean.on_a_meet")
posturl := fmt.Sprintf("%s/%s/%s", host, "api/states", entityID)
state := "on"
if !status {
state = "off"
}
type Body struct {
State string `json:"state"`
}
body := Body{State: state}
bodyBytes, err := json.Marshal(body)
if err != nil {
return
}
r, err := http.NewRequest("POST", posturl, bytes.NewBuffer(bodyBytes))
r.Header.Add("Content-Type", "application/json")
r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
client := &http.Client{}
res, err := client.Do(r)
if err != nil {
return
}
defer res.Body.Close()
return
}
type ModuleMeta struct {
Name string
Size int64
UsedBy []string
InUse bool
}
func getModuleMeta(name string) (meta ModuleMeta, err error) {
file, err := os.Open("/proc/modules")
if err != nil {
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
s := strings.Split(scanner.Text(), " ")
if s[0] == name {
size, err := strconv.ParseInt(s[1], 10, 64)
if err != nil {
return ModuleMeta{}, err
}
return ModuleMeta{
Name: s[0],
Size: size,
UsedBy: strings.Split(s[3], ","),
InUse: s[2] != "0",
}, nil
}
}
err = errors.New("module not found")
return
}
func loopCheckState(ctx context.Context) (err error) {
meta, err := getModuleMeta("uvcvideo")
if err != nil {
return
}
if prevState == nil || *prevState != meta.InUse {
log.Default().Printf("Module %s status changed to: %t\n", meta.Name, meta.InUse)
prevState = &meta.InUse
err = updateHassStatus(ctx, meta.InUse)
}
return
}
func main() {
logWriter, err := syslog.New(syslog.LOG_SYSLOG, "on-a-meet")
if err != nil {
log.Fatalln("Unable to set logfile:", err.Error())
}
log.SetOutput(logWriter)
ctx := context.TODO()
log.Default().Printf("Starting on-a-meet script")
for {
err = loopCheckState(ctx)
if err != nil {
logError(err)
}
time.Sleep(time.Duration(1 * time.Second))
}
}
A very simple program, only mentions that instead of using lsusb
I read /proc/modules
to get the same data
Running the program as a user service
To run the program when the computer starts up, the best way is to convert it into a service.
You only need to create a file, for example on-a-meet.service
with the following content
[Unit]
Description="On A Meet Service"
[Service]
Type=simple
ExecStart= Path/to/your/compiled/script
Restart=on-failure
StandardOutput=file:%h/log_file
[Install]
WantedBy=default.target
Then
copy it
/etc/systemd/user
(as root user)Run
systemctl --user edit on-a-meet.service
and add the following (with the correct values for your case)[Service] Environment="ON_A_MEET_HASS_SERVER=" Environment="ON_A_MEET_ENTITY_ID=" Environment="ON_A_MEET_HASS_TOKEN="
To set the environment variables the service will need
Run
systemctl --user daemon-reload
(as your user)Run
systemctl --user start on-a-meet.service
Run
systemctl --global enable on-a-meet.service
(as root)
And that’s all!!! After that, you will get a visual notification when your camera is active
See it in action
The next steps
- I would like to turn on the light sign when at least one computer in the room is using the webcam, I think just adding the script to the other computers and tuning up a bit the Home Assistant’s scenes would be easy
- Add OSX support, unfortunately, I will use a Mac because the work, then I will need to find out how to check when the camera is active on OSX
- Understand why the module says is in use for a second when the camera is not in use, causing false positives
Comments, ideas, or feedback is welcomed!!