Mi esposa y yo trabajamos en remoto y lo hacemos en la misma habitación, pero hay un problema: las reuniones, y obviamente, trabajando en remoto tenemos, digamos, más de las que nos gustaría, y no siempre es fácil saber cuándo el otro está en una reunión.
Decidí buscar una solución para que fuera visible cuando alguien está en una reunión de forma sencilla, con solo un vistazo.
La solución: Poner un letrero luminoso en la pared de la habitación de trabajo y encenderlo cuando estoy en una reunión y apagarlo cuando no, simple. :)
El letrero luminoso
Encontré un letrero tipo neón barato en una tienda online y lo pedí. Este letrero se alimenta por USB y se controla manualmente con un botón de interruptor, de modo que pudiera controlarlo automáticamente. El tipo neón es solo una tira LED con forma, así que pedí un controlador led wifi (y una fuente de alimentación) para reemplazar el controlador del letrero, y ahora podemos controlar la luz vía wifi usando la app de Tuya.
Solucionamos la parte del hardware, ahora debemos resolver cómo detectar cuándo estoy en una reunión.
Detectando cuándo estoy en una reunión
Supongo que hay muchas formas de detectar cuándo estás en una reunión, pero decidí usar la webcam, así la solución funciona con cualquier aplicación de reuniones: Zoom, Google Meet, Discord, Slack, etc.
Haciendo una búsqueda sencilla encontré que en Linux (si el kernel usa módulos, lo cual es común) puedes ejecutar:
$ lsmod | grep uvcvideo
Y obtener algo como:
uvcvideo 139264 0
videobuf2_vmalloc 20480 1 uvcvideo
uvc 12288 1 uvcvideo
El módulo uvcvideo es el que nos interesa (la primera línea); en esa línea, el primer número significa el tamaño del módulo en bytes (podemos ignorarlo) y el segundo significa el número de instancias del módulo en uso. En nuestro caso: 0 significa que la webcam no está en uso, 1 implica que la webcam está en uso.
Entonces podemos crear un script o un programa para comprobar este valor y encender o apagar el controlador wifi, pero antes de ver el código:
Usando Home Assistant para controlar la luz
Soy un gran fan de Home Assistant, ya que es una forma muy flexible de gestionar la domótica utilizando dispositivos de múltiples marcas. Esta es la razón por la que decidí usarlo; en lugar de pelearme con la API del fabricante del controlador wifi (si estuviera disponible), delegaré eso en Home Assistant, ya que puedes encontrar integraciones para muchos fabricantes estandarizando la forma en que controlamos la luz.
Lo siguiente que hice fue crear un interruptor virtual en Home Assistant, esto me permitirá almacenar el estado de uso de la webcam en Home Assistant y luego activar una escena para encender o apagar la luz.
Ve a
Settings>Devices & services> pestañaHelpers> Haz clic enCreate helper> Seleccionatoggle
Hacer eso en lugar de cambiar el estado de la luz directamente nos permite ser más flexibles y crear mejores automatizaciones, por ejemplo, para encender también otra luz o pausar la aspiradora cuando estás en una reunión para reducir el ruido ambiental.
Después de eso, necesitamos crear un token de acceso para la API de Home Assistant.
Haz clic sobre tu nombre de usuario > pestaña
Security> Haz clic enCreate token
Estas son las automatizaciones que creé:
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 # Añadí un tiempo mínimo para evitar falsos positivos
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
Con eso, podemos crear un programa para comprobar el estado de la webcam y enviarlo a Home Assistant.
El observador
He creado un programa escrito en Go para comprobar el estado del módulo uvcvideo y enviar los cambios a 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))
}
}
Un programa muy sencillo, solo mencionar que en lugar de usar lsusb leo /proc/modules para obtener los mismos datos.
Ejecutando el programa como un servicio de usuario
Para ejecutar el programa cuando el ordenador se inicia, la mejor forma es convertirlo en un servicio.
Solo necesitas crear un archivo, por ejemplo on-a-meet.service, con el siguiente contenido:
[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
Luego:
-
Cópialo en
/etc/systemd/user(como usuario root) -
Ejecuta
systemctl --user edit on-a-meet.servicey añade lo siguiente (con los valores correctos para tu caso)[Service] Environment="ON_A_MEET_HASS_SERVER=" Environment="ON_A_MEET_ENTITY_ID=" Environment="ON_A_MEET_HASS_TOKEN="Para establecer las variables de entorno que el servicio necesitará.
-
Ejecuta
systemctl --user daemon-reload(como tu usuario) -
Ejecuta
systemctl --user start on-a-meet.service -
Ejecuta
systemctl --global enable on-a-meet.service(como root)
¡¡¡Y eso es todo!!! Después de eso, recibirás una notificación visual cuando tu cámara esté activa.
Míralo en acción
Los siguientes pasos
- Me gustaría encender el letrero luminoso cuando al menos un ordenador de la habitación esté usando la webcam; creo que simplemente añadiendo el script a los otros ordenadores y ajustando un poco las escenas de Home Assistant sería fácil.
- Añadir soporte para OSX; desafortunadamente, usaré un Mac por el trabajo, así que tendré que averiguar cómo comprobar cuándo la cámara está activa en OSX.
- Entender por qué el módulo dice que está en uso durante un segundo cuando la cámara no está en uso, causando falsos positivos.
¡Cualquier comentario, idea o feedback es bienvenido!
Sergio Carracedo