Microsoft Teams-Status. Im Wohnzimmer.

Aus aktuellem Anlass arbeite ich ununterbrochen aus dem Home Office und aus anderem aktuellen Anlass ist meine Frau gerade ziemlich viel zu Hause. Eigentlich ist es ziemlich gut dass in den meisten Mittagspausen für mich gekocht wird. Aber es hat auch ein Problem geschaffen, das es bisher nicht gab: Ich sitze im Büro1Oh ja, wir gehören zu den Leuten die einen Raum Büro nennen. Deal with it. und telefoniere dort ab und zu, aber meine Frau will auch ab und zu ins Büro um irgendwas von dort zu holen. Dass sie jedes Mal anklopft ist umständlich, dass sie jedes Mal per Messenger fragt ob sie reinkommen kann ist umständlich, aber da Hausautomatisierung zu meinen Superkräften gehört habe ich natürlich eine Lösung gefunden.

Für die Arbeit setzen wir zur Kommunikation Microsoft Teams ein. Weil ich beinahe ausschließlich mit Mitarbeitenden telefoniere finden praktisch alle meine Anrufe über Teams statt und Teams stellt meinen Status automatisch auf Beschäftigt, wenn ich telefoniere. Mein Arbeitgeber erlaubt leider nicht, per Graph API auf meinen Status zuzugreifen, aber hier hat zum Glück jemand eine Lösung gefunden, die vollständig lokal auf meinem Computer arbeitet.

Auf meinem Computer

Teams schreibt eine Log-Datei, in der unter anderem auch jeder Status-Wechsel geloggt wird. Dieses Logfile kann man mit einem einzeiligen Powershell-Script überwachen, mit einem Regex prüfen ob die neueste Zeile ein Statuswechsel ist und den entsprechenden Status an ein weiteres Script übergeben.

Get-Content $env:APPDATA\Microsoft\Teams\logs.txt -Wait -Tail 0 | ? { $_ -match "(?<=StatusIndicatorStateService: Added )(\w+)" } | % { if($matches[0] -ne "NewActivity") {& "C:\Scripts\TeamsColor.ps1" $matches[0] }}

Dieses zweite Script schickt den Status an die API von Home Assistant und setzt einen Sensor namens „teams_raw“.

$newval = [Convert]::ToString($args[0])

$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("Authorization", "Bearer TOKEN")
$headers.Add("Content-Type", "application/json")

$body = "{`"state`": `"$newval`"}"

$response = Invoke-RestMethod 'http://yggdrasil.local/api/states/sensor.teams_raw' -Method 'POST' -Headers $headers -Body $body

Die sicherste Alternative ist es sich ganz unten im Home Assistant-Benutzerprofil einen neuen Token dafür zu erstellen.

Das erste Powershell-Script habe ich mit nssm als Dienst auf meinem Computer eingerichtet. Der Pfad für das Powershell-Executable scheint auf manchen Computern anders zu sein, bei mir ist es C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe. Als Parameter benutze ich -NoProfile um zu verhindern dass Powershell Profileinstellungen lädt, -ExecutionPolicy Bypass damit es unsignierte Scripte wie meines ausführt und schließlich -File C:\Scripts\UpdateTeamsStatus.ps1 um das Script auszuführen.

Das ist die einzige Stelle, an der ich aktuell unzufrieden mit dem Setup bin: Dieser Dienst wird mit meinem eigenen Benutzerkonto ausgeführt. Ursprünglich wollte ich den Benutzer Lokaler Dienst verwenden, mit diesem konnte das Script aber nicht starten. Der Benutzer hat auf C:\Scripts und das Powershell-Executable Ausführen-Rechte und darf das Logfile von Teams lesen. Für Vorschläge, was ich tun soll, bin ich offen.

Home Assistant

In Home Assistant benutze ich einen Template-Sensor, um aus dem sensor.teams_raw die vielen unterschiedliche Status zu einer kleineren, nützlicheren Liste von Status zu komprimieren (Vollständige Sensor-Definition).

teams:
  friendly_name: "Microsoft Teams"
  unique_id: sensor.teams
  value_template: >-
      {% if states("sensor.teams_raw") in ["Available"] %}
        Available
      {% elif states("sensor.teams_raw") in ["DoNotDisturb","Presenting","Focusing"] %}
        Do not disturb
      {% elif states("sensor.teams_raw") in ["Busy","OnThePhone","DoNotDisturb","Presenting","Focusing","InAMeeting"] %}
        Busy
      {% elif states("sensor.teams_raw") in ["Away","BeRightBack"] %}
        Away
      {% else %}
        {{ states("sensor.teams_raw") }}
      {% endif %}

Als eine Art Proof of Concept hab ich auf diesem Sensor eine Automatisierung aufgesetzt, welche die Farbe des indirekten Lichts an meinem Schreibtisch abhängig vom Status ändert. Als zusätzliche Bedingung habe ich eingefügt dass mein Computer auch wirklich eingeschaltet sein muss2Das ist ein einfacher Ping auf seine IP.. Grundsätzlich können Änderungen an meinem Teams-Status sowieso nur stattfinden wenn der Computer an ist, schließlich muss der Dienst laufen, aber falls sich das ändern sollte wollte ich nicht dass das Licht zu Hause an und aus geht wenn ich im Büro sitze und telefoniere.

- id: study_table_lights_teams_status_busy
  alias: "Büro Tisch - Licht auf Beschäftigt schalten"
  trigger:
    - platform: state
      entity_id: sensor.teams
      to:
        - 'Busy'
        - 'Do not disturb'
  condition:
    - condition: state
      entity_id: binary_sensor.ping_pc_pascal
      state: 'on'
  action:
    - service: light.turn_on
      data:
        entity_id: light.wled_office_table
        brightness_pct: 100
        rgb_color: [255, 0, 0]

- id: study_table_lights_teams_status_away
  alias: "Büro Tisch - Licht auf Abwesend schalten"
  trigger:
    - platform: state
      entity_id: sensor.teams
      to:
        - 'Away'
  condition:
    - condition: state
      entity_id: binary_sensor.ping_pc_pascal
      state: 'on'
  action:
    - service: light.turn_on
      data:
        entity_id: light.wled_office_table
        brightness_pct: 100
        rgb_color: [255, 190, 0]

- id: study_table_lights_teams_status_available
  alias: "Büro Tisch - Licht auf Anwesend schalten"
  trigger:
    - platform: state
      entity_id: sensor.teams
      to:
        - 'Available'
  condition:
    - condition: state
      entity_id: binary_sensor.ping_pc_pascal
      state: 'on'
  action:
    - service: light.turn_on
      data:
        entity_id: light.wled_office_table
        brightness_pct: 100
        hs_color: ['{{states("input_number.global_hue_primary")|float}}', 100]

Das ist immerhin eine gute Rückversicherung dass mein Status auch wirklich auf Abwesend steht, wenn ich einen Kaffee holen gehe, hilft meiner Frau aber natürlich immer noch wenig. Deshalb habe ich für eine Lampe im Wohnzimmer noch Automatisierungen hinzugefügt die sie rot leuchten lassen, wenn ich telefoniere (Vollständige Automatisierungen für diese Lampe).

- id: living_room_table_light_occupied
  alias: "Wohnzimmer - Tischlampe auf Beschäftigt schalten"
  trigger:
    - platform: state
      entity_id: sensor.teams
      to:
        - 'Busy'
        - 'Do not disturb'
  condition:
    - condition: state
      entity_id: input_boolean.disable_living_room
      state: 'off'
    - condition: state
      entity_id: person.pascal
      state: 'home'
    - condition: state
      entity_id: binary_sensor.ping_pc_pascal
      state: 'on'
  action:
    - service: light.turn_on
      data:
        entity_id: light.living_room_lamp_1
        brightness_pct: 30
        rgb_color: [255, 0, 0]
        transition: 1

- id: living_room_table_light_unoccupied
  alias: "Wohnzimmer - Tischlampe auf Nicht beschäftigt schalten"
  trigger:
    - platform: state
      entity_id: sensor.teams
      to:
        - 'Available'
  condition:
    - condition: state
      entity_id: input_boolean.disable_living_room
      state: 'off'
    - condition: state
      entity_id: person.pascal
      state: 'home'
    - condition: state
      entity_id: binary_sensor.ping_pc_pascal
      state: 'on'
  action:
  - choose:
    - conditions:
        - condition: state
          entity_id: input_select.state_living_room
          state: 'Gedaemmt'
      sequence:
      - service: light.turn_on
        data:
          entity_id: light.living_room_lamp_1
          brightness_pct: 20
          hs_color: ['{{states("input_number.global_hue_secondary")|float}}', 100]
          transition: 1
    - conditions:
        - condition: state
          entity_id: input_select.state_living_room
          state: 'An'
      sequence:
        - service: light.turn_on
          data:
            entity_id: light.living_room_lamp_1
            brightness_pct: 100
            color_temp: 480
            transition: 1
    default:
    - service: light.turn_off
      data:
        entity_id: light.living_room_lamp_1

Die genauen Details der Automatisierung lasse ich lieber aus – mein komplettes System ist nicht ganz trivial zu erklären. Böse Zungen behaupten sogar es wäre massiv overengineered und ich solle mal auf mein Leben klar kommen, aber solche Leute können mir nichts vorschreiben! Wichtig ist dass die Lampe sich rot einschaltet wenn mein Status entweder Beschäftigt oder Bitte nicht stören ist und sie das macht, was sie ansonsten machen sollte, wenn mein Status zurück zu verfügbar wechselt.

Einige Randfälle sind in den Automatisierungen jetzt noch nicht abgedeckt, zum Beispiel was passiert wenn mein Status direkt von Beschäftigt auf Abwesend geht oder was passiert wenn mein Status von Beschäftigt auf Verfügbar geht, aber die Sonne schon unter dem Horizont steht und die Balkontür offen ist3Ich liebe Hausautomatisierung weil das ein real existierender Anwendungsfall ist!. Das sollte alles kein Problem sein, schließlich arbeite ich nicht jeden Tag und nicht bis in die Nacht, aber sollte irgendwas davon wirklich stören kann ich es ja immer noch korrigieren.

Und so habe ich mit einer Stunde Arbeit die Lebensqualität von mir, meiner Frau und den Leuten mit denen ich arbeite ein ganz kleines bisschen verbessert.