Skip to content

Instantly share code, notes, and snippets.

@Anamon
Last active December 13, 2022 07:38
Show Gist options
  • Save Anamon/afe0b261463597ba91bcf915d5f3ce26 to your computer and use it in GitHub Desktop.
Save Anamon/afe0b261463597ba91bcf915d5f3ce26 to your computer and use it in GitHub Desktop.
Update USB traffic light by Teams status
#!".venv\Scripts\python.exe"
"""
Sets the red, yellow, and green LEDs of a USB traffic light, depending on Teams status.
The hard-coded VID/PID and hex strings work with the "USB Tisch-Ampel" by Cleware GmbH.
Note: The script determines Teams status by reading the local Teams logfile, and checks which tray icon was last set.
It's easily confused, e.g. when a status change was not reflected in the tray symbol because the "New Activity"
icon was active.
Note: This very simple implementation reads the full log file into memory every time it is being modified (max. every
five seconds). On my system, the logfile is flushed regularly and never seems to approach 10 MB, so this was
deemed good enough.
Author: Daniel Saner, https://www.github.com/Anamon
Version: 0.2, 2022-12-02
"""
import curses
import hid
import os
import re
import time
from typing import Annotated, Tuple, TypedDict
# USB-Ampel by Cleware GmbH
vid = 0x0d50
pid = 0x0008
led_red = b'\x00\x00\x10'
led_yellow = b'\x00\x00\x11'
led_green = b'\x00\x00\x12'
led_on = b'\x01'
led_off = b'\x00'
teams_log = os.path.expandvars(r"%APPDATA%\Microsoft\Teams\logs.txt")
class ColorsByStatus(TypedDict):
status: str
colors: Tuple[int, int, int]
colorsByStatus: Annotated[ColorsByStatus, "Sets the light colours (red, yellow, green) by status value"] = {
"Available": (False, False, True), # Available: green
"Busy": (False, True, False), # Busy: yellow
"DoNotDisturb": (True, False, False), # DND: red
"OnThePhone": (True, False, False), # In call: red
"BeRightBack": (False, False, False), # BRB: off
"Away": (False, False, False), # Away: off
"Offline": (False, False, False) # Offline: off
}
def setLights(*, device: hid.Device, red: bool, yellow: bool, green: bool) -> None:
"""Sets the lights of the given device to the passed states. If a light is set to None, the status will not change."""
if red is not None: device.write(led_red + (led_on if red else led_off))
if yellow is not None: device.write(led_yellow + (led_on if yellow else led_off))
if green is not None: device.write(led_green + (led_on if green else led_off))
def main(stdscr):
stdscr.nodelay(1)
with hid.Device(vid, pid) as lights:
stdscr.addstr(f"Found device {lights.product} by {lights.manufacturer}\n")
setLights(device=lights, red=False, yellow=False, green=False)
last_change = 0
last_state = ""
stdscr.addstr("Press X (and wait up to 5 seconds) to exit.")
while True:
char = stdscr.getch()
if char == 120:
setLights(device=lights, red=False, yellow=False, green=False)
break
curses.flushinp()
current_timestamp = os.stat(teams_log).st_mtime
if current_timestamp > last_change:
last_change = current_timestamp
with open(teams_log, "r") as log:
for line in reversed(log.readlines()):
match = re.search("\(current state:.*-> (.*)\)", line)
if match is not None and match.group(1) in colorsByStatus:
if last_state != match.group(1):
last_state = match.group(1)
setLights(device=lights, red=colorsByStatus[last_state][0], yellow=colorsByStatus[last_state][1], green=colorsByStatus[last_state][2])
break
time.sleep(5)
if __name__ == "__main__":
curses.wrapper(main)

0.2

2022-12-02

Changed

  • Replaced keyboard with curses for hotkey handling.
    • The exit hotkey is now no longer trapped globally, but only triggered when the script has input focus.

0.1

2022-11-23

  • Initial release
hid==1.0.5
windows-curses==2.3.1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment