2022-12-02
- Replaced
keyboard
withcurses
for hotkey handling.- The exit hotkey is now no longer trapped globally, but only triggered when the script has input focus.
2022-11-23
- Initial release
#!".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) |
hid==1.0.5 | |
windows-curses==2.3.1 |