Ein gesprochenes Morgen-Briefing in Home Assistant: Kalender, Wetter und Strompreis
Tägliches 06:00-Sprachbriefing in Home Assistant: Google AI TTS, drei Familienkalender, EV-Status, Waze-Verkehr, Strompreis und Müllabfuhr.
Jeden Morgen öffnete ich die Home Assistant App, um den Tageskalender zu prüfen, einen Blick auf das Wetter zu werfen und den Strompreis für die Nacht zu beurteilen. Drei separate Abfragen vor dem Kaffee.
Die Morgen-Briefing-Automation hat das alles ersetzt. Um 06:00 Uhr liest der Küchenlautsprecher die heutigen Termine aus drei Familienkalendern vor, nennt die Außentemperatur, warnt vor der morgigen Müllabfuhr und gibt den aktuellen Stromspot-Preis an. Das Ganze dauert etwa 15 Sekunden.
Das Kernmuster: calendar.get_events mit response_variable
Die meisten Home Assistant Automations-Tutorials stoppen beim Auslösen durch ein Kalenderereignis. Diese Automation verfolgt einen anderen Ansatz: Sie fragt den Kalender aktiv nach den heutigen Ereignissen ab und verwendet die Antwort in einem Template.
Die calendar.get_events-Aktion gibt eine strukturierte Antwort zurück, die in einer Variable gespeichert werden kann:
- action: calendar.get_events
target:
entity_id: calendar.rolf_szimnau_dk
data:
start_date_time: "{{ today_at('00:00') }}"
end_date_time: "{{ today_at('23:59') }}"
response_variable: kalender
Die Antwort ist ein Dict mit der Entitäts-ID als Schlüssel. Auf die Ereignisliste wird so zugegriffen:
{% set events = kalender['calendar.rolf_szimnau_dk']['events'] %}
Mehrere Kalender zusammenführen
Für jeden Kalender eine separate calendar.get_events-Aktion mit eigenem response_variable ausführen, dann die Listen im Template kombinieren:
{% set events = events_rolf + events_annette + events_louise %}
Die vollständige Automation
alias: "Morgen-Briefing: TTS um 06:00"
mode: single
trigger:
- platform: time
at: "06:00:00"
action:
- action: calendar.get_events
target:
entity_id: calendar.rolf_szimnau_dk
data:
start_date_time: "{{ today_at('00:00') }}"
end_date_time: "{{ today_at('23:59') }}"
response_variable: kalender
- action: calendar.get_events
target:
entity_id: calendar.annette_familie
data:
start_date_time: "{{ today_at('00:00') }}"
end_date_time: "{{ today_at('23:59') }}"
response_variable: kalender_annette
- action: calendar.get_events
target:
entity_id: calendar.louise_familie
data:
start_date_time: "{{ today_at('00:00') }}"
end_date_time: "{{ today_at('23:59') }}"
response_variable: kalender_louise
- action: tts.speak
target:
entity_id: tts.google_ai_tts
data:
media_player_entity_id: media_player.hojtaler_kokken
language: da-DK
message: >-
{% set dage = ['mandag', 'tirsdag', 'onsdag', 'torsdag', 'fredag', 'lørdag', 'søndag'] %}
{% set dag = dage[now().weekday()] %}
{% set events_rolf = kalender['calendar.rolf_szimnau_dk']['events'] %}
{% set events_annette = kalender_annette['calendar.annette_familie']['events'] %}
{% set events_louise = kalender_louise['calendar.louise_familie']['events'] %}
{% set events = events_rolf + events_annette + events_louise %}
{% set temp = state_attr('weather.forecast_home', 'temperature') %}
{% set affald = states('sensor.affalddk_bytoften_1_naeste_afhentning') | int(99) %}
{% set elpris = states('sensor.energi_data_service') | float(0) | round(2) %}
{% set km = states('sensor.skoda_elroq_range') | int(0) %}
{% set bat = states('sensor.skoda_elroq_battery_percentage') | int(0) %}
{% set maal = states('sensor.skoda_elroq_target_battery_percentage') | int(80) %}
{% set lader = states('sensor.skoda_elroq_charging_state') %}
{% set ac = states('climate.skoda_elroq_air_conditioning') %}
{% set rejsetid = states('sensor.rejsetid_vejle_travel_time') | float(0) | round(0) | int %}
{% set normal = states('sensor.rejsetid_vejle_normal') | float(0) | round(0) | int %}
God morgen! I dag er det {{ dag }}.
Temperaturen ude er {{ temp }} grader.
{% if events | length > 0 %}
Du har {{ events | length }} aftale{% if events | length > 1 %}r{% endif %} i dag:
{% for event in events %}{{ event.summary }}{% if not loop.last %}, {% endif %}{% endfor %}.
{% else %}Ingen aftaler i dag.
{% endif %}
{% if affald == 0 %}Husk: affald hentes i dag!
{% elif affald == 1 %}Husk: affald hentes i morgen.
{% endif %}
{% if km > 0 and km < 100 %}Elroq har kun {{ km }} kilometer tilbage.
{% endif %}
{% if lader == 'charging' %}Elroq lader: {{ bat }}% ud af {{ maal }}%.
{% elif bat < maal %}Elroq er {{ bat }}% – har ikke nået målet på {{ maal }}%.
{% endif %}
{% if ac != 'off' %}Elroq varmer op.
{% endif %}
{% if now().weekday() < 5 %}
{% if rejsetid > 0 %}Kørsel til arbejde tager {{ rejsetid }} minutter{% if normal > 0 and rejsetid - normal >= 5 %}, {{ rejsetid - normal }} minutter mere end normalt{% endif %}.
{% else %}Trafikdata mangler.
{% endif %}{% endif %}
Elprisen er {{ elpris }} kroner per kilowattime.
Anpassung
Nur Werktage — Bedingung über den Aktionen hinzufügen:
condition:
- condition: time
weekday: [mon, tue, wed, thu, fri]
Anderer Lautsprecher — media_player.hojtaler_kokken durch eine beliebige HA Media Player-Entität ersetzen.
Andere TTS-Engine — tts.google_ai_tts durch tts.google_translate_en_com für die kostenlose Option ersetzen.
EV-Ladestatus und Vorklimatisierung hinzufügen
Zwei Sensoren der MyŠkoda-Integration (oder einer anderen EV-Integration) geben Auskunft darüber, ob das Auto für den Tag bereit ist.
Ladestatus — das Briefing spricht nur, wenn Handlungsbedarf besteht. Hat das Auto sein Ziel über Nacht erreicht, wird nichts gesagt.
{% set bat = states('sensor.skoda_elroq_battery_percentage') | int(0) %}
{% set maal = states('sensor.skoda_elroq_target_battery_percentage') | int(80) %}
{% set lader = states('sensor.skoda_elroq_charging_state') %}
{% if lader == 'charging' %}Elroq lader: {{ bat }}% ud af {{ maal }}%.
{% elif bat < maal %}Elroq er {{ bat }}% – har ikke nået målet på {{ maal }}%.
{% endif %}
sensor.skoda_elroq_target_battery_percentage wird in der MyŠkoda-App gesetzt und direkt als HA-Sensor bereitgestellt. Falls die eigene EV-Integration kein Ladeziel kennt, maal durch eine feste Zahl wie 80 ersetzen.
Vorklimatisierungsbestätigung — wenn eine Morgenaufwärmautomation wenige Minuten vor 06:00 Uhr läuft, kann das Briefing bestätigen, dass sie tatsächlich gestartet ist:
{% set ac = states('climate.skoda_elroq_air_conditioning') %}
{% if ac != 'off' %}Elroq varmer op.
{% endif %}
Das funktioniert, weil die Aufwärmautomation um 05:55 Uhr läuft — fünf Minuten vor dem Briefing. Um 06:00 Uhr ist climate.skoda_elroq_air_conditioning entweder aktiv (eine kurze Bestätigungszeile) oder noch off (kein Text — die Aufwärmautomation verwaltet ihren eigenen Fehleralarm separat).
Verkehrsinformationen hinzufügen
Die Integration Waze Travel Time liefert einen Sensor mit der aktuellen Fahrzeit einschließlich Verkehr. Ein API-Schlüssel ist nicht nötig. Sie finden sie unter Einstellungen → Geräte & Dienste → Integration hinzufügen → Waze Travel Time.
Drei Einstellungen entscheiden, ob sie funktioniert. Verwenden Sie zone.home als Startpunkt. Eine eingetippte Adresse schlägt Waze selbst nach, und meine landete 3,3 km vom Haus entfernt, sodass die Fahrzeit vom falschen Ort aus berechnet wurde. Als Ziel kopieren Sie die Koordinaten Ihres Arbeitsplatzes aus Google Maps.
Lassen Sie die Felder für einzuschließende und auszuschließende Straßen leer, solange Sie nicht genau wissen, was hineingehört. Sie werden Zeichen für Zeichen mit den Straßennamen der Route verglichen, die Straßennummer in Klammern eingeschlossen (E45, E20 / E45). Steht dort eine Adresse, verwirft Waze jede Route. Der Sensor bleibt dann auf unknown, und die einzige Spur ist eine Warnung im Log: „No routes matched your filters“. Meiner lief so, bis ich nach dem Fehler gesucht habe. Die genauen Straßennamen liefert die Aktion waze_travel_time.get_travel_times unter Entwicklerwerkzeuge → Aktionen, aufgerufen mit Start und Ziel. Die Antwort listet sie für jede Route auf.
Home Assistant bildet die Entitäts-ID aus dem Namen der Integration. Ich habe meine in sensor.pendelzeit und sensor.pendelzeit_normal umbenannt, und diese Namen verwenden die Beispiele unten.
Wie viel schlechter als sonst?
Eine feste Grenze wie „mehr als 40 Minuten“ sagt wenig, weil auch ein normaler Morgen schwankt. Fügen Sie die Integration ein zweites Mal mit demselben Start und Ziel hinzu und schalten Sie in den Optionen Echtzeit aus. Dieser Sensor liefert die Fahrzeit an einem gewöhnlichen Tag, und die Differenz ist die Verspätung:
{% set fahrzeit = states('sensor.pendelzeit') | float(0) | round(0) | int %}
{% set normal = states('sensor.pendelzeit_normal') | float(0) | round(0) | int %}
{% if now().weekday() < 5 %}
{% if fahrzeit > 0 %}Fahrt zur Arbeit: {{ fahrzeit }} Minuten{% if normal > 0 and fahrzeit - normal >= 5 %}, {{ fahrzeit - normal }} mehr als sonst{% endif %}.
{% else %}Verkehrsdaten fehlen.
{% endif %}
{% endif %}
Der letzte Zweig ist genauso wichtig wie der Rest. Meine erste Version ließ den Satz weg, wenn die Fahrzeit 0 war. Ein kaputter Sensor machte das Briefing also nur etwas kürzer, und niemand merkte es. „Verkehrsdaten fehlen“ fällt am nächsten Morgen auf.
Landstraße oder Autobahn
Ich kann über die Landstraße oder über die Autobahn zur Arbeit fahren, und was schneller ist, wechselt von Tag zu Tag. Waze liefert bis zu vier Alternativen, und von meinem Haus aus war die Autobahn nie dabei, auch nicht an Tagen, an denen Google Maps sie ganz oben zeigte. Ein Filter wählt nur unter den Routen, die Waze liefert, und kann die Autobahn deshalb nicht erzwingen.
Die Lösung ist, die Autobahnroute an einem Punkt auf der Autobahn selbst zu teilen: ein Waze-Sensor vom Haus bis zu diesem Punkt und einer vom Punkt bis zur Arbeit, im Template addiert. Wählen Sie den Punkt auf der Fahrbahn in Ihrer Fahrtrichtung. Die beiden Fahrbahnen liegen nur etwa zehn Meter auseinander, und ein Punkt auf der falschen ergibt einen Umweg mit Wenden, den Sie an einer seltsamen Entfernung in get_travel_times erkennen. Beim Landstraßen-Sensor tragen Sie die Autobahnnamen in das Feld für ausgeschlossene Straßen ein, damit er nie von selbst auf die Autobahn wechselt.
Jede Fahrzeit hat jetzt einen Sensor für einen normalen Tag neben sich, und mit der geteilten Autobahn sind das sechs Waze-Sensoren. Das sind viele Abfragen alle fünf Minuten für einen einzigen Satz am Tag. Ich habe bei allen das automatische Aktualisieren abgeschaltet (Menü ⋮ der Integration → Systemoptionen) und lasse die Automation sie direkt vor der Ansage aktualisieren:
- action: homeassistant.update_entity
target:
entity_id:
- sensor.pendelzeit
- sensor.pendelzeit_normal
continue_on_error: true
- delay:
seconds: 20
Unfälle und Baustellen
Die Waze-Integration liefert die Fahrzeit, aber nicht den Grund für eine Verspätung. In Dänemark lädt die Verkehrskarte der Straßenbehörde (trafikkort.vejdirektoratet.dk) ihre Meldungen aus einem öffentlichen JSON-Feed ohne Schlüssel. Der Feed ist nicht dokumentiert und kann sich ohne Vorwarnung ändern. Dieser REST-Sensor behält nur aktive Meldungen im Umkreis von 1,5 km um Ihre Route. Ersetzen Sie ROUTE durch Punkte entlang Ihrer eigenen Route, etwa einen pro Kilometer, als [Breitengrad, Längengrad]:
rest:
- resource: https://storage.googleapis.com/trafikkort-data/geojson/big-screen-events.json
scan_interval: 300
timeout: 30
sensor:
- name: Verkehrsmeldungen Pendel
value_template: >-
{#- Punkte entlang der Route, etwa 1 km Abstand -#}
{%- set ROUTE = [[56.0000, 9.0000], [56.0090, 9.0010], [56.0180, 9.0030]] -%}
{%- set ns = namespace(seen=[], out=[]) -%}
{%- for fc in value_json | reverse -%}
{%- for f in fc.features -%}
{%- set p = f.properties -%}
{%- set id = p.featureId | default('') | regex_replace('\\.\\d+$', '') -%}
{%- if id not in ns.seen -%}
{%- set ns.seen = ns.seen + [id] -%}
{%- set c = f.geometry.coordinates -%}
{%- if c[0] is not number -%}{%- set c = c[0] -%}{%- endif -%}
{%- set lon = c[0] | float(0) -%}{%- set lat = c[1] | float(0) -%}
{%- set near = namespace(v=false) -%}
{%- for q in ROUTE -%}{%- if ((lat - q[0]) * 111.2) ** 2 + ((lon - q[1]) * 62.9) ** 2 < 2.25 -%}{%- set near.v = true -%}{%- endif -%}{%- endfor -%}
{%- set h = p.header | default('') | regex_replace('<[^&]*>\\s*', '') | regex_replace('<[^>]*>\\s*', '') | replace('&', '&') | trim -%}
{%- set start = strptime(p.beginPeriod | default(''), '%d-%m-%Y kl. %H:%M', none) -%}
{%- set fresh = start is not none and (as_timestamp(now()) - as_timestamp(as_local(start))) < 43200 -%}
{%- if p.suspended | string | lower == 'false' and near.v and fresh and h not in ns.out -%}
{%- set ns.out = ns.out + [h] -%}
{%- endif -%}{%- endif -%}
{%- endfor -%}{%- endfor -%}
{{ (ns.out | join('. ') or 'Keine')[:250] }}
Zwei Details haben am meisten Zeit gekostet. Jede Meldung gibt es in mehreren Versionen, deren ID auf .1, .2 und so weiter endet, und nur die neueste sagt, ob sie vorbei ist (suspended). Das Template gruppiert deshalb nach der ID ohne Endung und nimmt die neueste. Außerdem enthält der Feed nur etwa einen Tag Verlauf, also testen Sie mit einer Meldung, die gerade aktiv ist, nicht mit einer von gestern.
Das Briefing liest den Sensor dann vor, sobald er etwas anderes als Keine enthält:
{% set meldung = states('sensor.verkehrsmeldungen_pendel') %}
{% if meldung not in ['Keine', 'unknown', 'unavailable'] %}Verkehrsmeldung: {{ meldung }}.{% endif %}
Warum tts.speak und nicht der alte Service
Home Assistant 2024+ verwendet tts.speak auf der TTS-Engine-Entität, mit media_player_entity_id als Datenfeld. Der alte tts.google_translate_say-Service funktioniert noch, ist aber veraltet. Das neue Muster trennt Engine und Abspielgerät — beide können unabhängig voneinander ausgetauscht werden.
tts.google_ai_tts wird von der Google Generative AI-Integration bereitgestellt — derselben Integration, die auch den Gemini-Konversationsagenten in Home Assistant antreibt. Sie erfordert einen Google AI Studio API-Schlüssel, erzeugt aber natürlich klingende Sprache im Vergleich zur roboterhaften Ausgabe der kostenlosen Google Translate TTS-Option.
Neue Beiträge direkt im Feedreader: per RSS folgen In der Google-Suche: szimnau.dk als bevorzugte Quelle wählen