Assist Router

Asystent głosowy Home Assistant, który rozumie pytania po polsku także wtedy, gdy nie pasują do żadnego zdania. Wszystko działa lokalnie.

Polski · English · GitHub

Konfigurujesz cały głosowy zestaw od zera (satelity, Whisper, Piper, Ollama, rozpoznawanie mówcy)? Zacznij od PRZEWODNIK.md, który prowadzi krok po kroku i odsyła tutaj po szczegóły.

1. Co to jest

Wbudowany asystent Home Assistant rozpoznaje zdania według szablonów: intencje HA, własne zdania w custom_sentences i automatyzacje z wyzwalaczem zdaniowym. Jest szybki i pewny, ale wystarczy powiedzieć coś inaczej („Na którą Ola idzie jutro do szkoły?” zamiast „O której Ola zaczyna lekcje?”) i słyszymy „Nie rozumiem”. Dopisywanie kolejnych wariantów zdań nie ma końca.

Assist Router to agent rozmowy, który nakłada się na wbudowanego agenta HA. Wszystko, co HA rozumie, działa bez zmian. Gdy HA nie rozpozna zdania:

2. Jak działa

Mowa Whisper (STT) Agent HA intencje, custom_sentences, automatyzacje Rozpoznane odpowiedź bez zmian Głośnik Piper (TTS) Router (mały model) tylko temat: {"temat": "lekcje"} Dane tematu encje, atrybuty, usługi przycięte do osoby i dnia Sterowanie rozbiór -> intencja HA dopytanie, intent.async_handle Wikipedia pytania o wiedzę Agent zapasowy (rozmowa) Model odpowiedzi odpowiada na pytanie tylko z danych; streszcza artykuł nie rozpoznano

Dlaczego router zwraca tylko temat

Małe modele mylą się, gdy mają jednocześnie wybrać temat, osobę, dzień i wywołać narzędzie. Dlatego podział pracy wygląda tak:

Dzięki temu odpowiedzi nie są zmyślone, a mały model wystarcza do większości zadań. Ten sam podział obowiązuje przy sterowaniu: model wskazuje intencję i parametry z list, kod dopasowuje urządzenie, a wykonuje HA.

Pytania o wiedzę (Wikipedia)

  1. Hasło. Kod wycina początek pytania („kim był”, „co to jest”, „kto jest obecnie”, „kto napisał / wynalazł / wygrał”, „gdzie leży”…), więc do wyszukiwarki trafia samo hasło: „Kto napisał Pana Tadeusza?” → „Pana Tadeusza”.
  2. Literówki. Gdy wyszukiwanie nic nie da („…w piłce norznej”), integracja bierze podpowiedź pisowni Wikipedii („…w piłce nożnej”) i szuka jeszcze raz.
  3. Pasujący artykuł. Artykuł musi mieć w tytule słowo z hasła - wyszukiwarka zawsze coś znajdzie, także dla bełkotu, a takiego artykułu nie czytamy.
  4. Kilka artykułów. Model dostaje pierwszy artykuł (do 4000 znaków) i wstępy dwóch kolejnych pasujących (po 1000). Przy „Kto jest obecnie mistrzem świata…” pierwszy wynik to przyszły turniej, a odpowiedź jest w artykule o ostatnim.
  5. Odpowiedź. Model odpowiada na pytanie z tych tekstów w 3–4 zdaniach.

3. Wymagania i modele

Sprawdzone w praktyce: cały zestaw działa na co dzień w domu autora na NVIDIA Jetson AGX Orin 64 GB: Ollama (Gemma 4 26B jako router i model odpowiedzi), faster-whisper i Assist Speaker ID. Działa bardzo dobrze: router odpowiada w ok. 0,7 s, a odpowiedź z danych (router i model odpowiedzi) zajmuje ok. 2 s. 64 GB wspólnej pamięci mieści model razem z Whisperem.

Przykładowe wyniki z NVIDIA Jetson AGX Orin 64 GB. Test obejmował 96 pytań do routera, 34 pytania o prawdziwe dane z HA i 8 streszczeń z Wikipedii:

RolaModelTrafnośćCzas
RouterGemma 4 26B (ten sam model co odpowiedzi, w pamięci raz)153/153 (z akcjami i bełkotem)0,7 s
Router (mniejszy)Gemma 4 E4B92/960,5 s
Model odpowiedziGemma 4 26B (MoE, 4B aktywnych)34/34 dane, 8/8 streszczeń1,2 s / 5,5 s
Rozbiór poleceń sterowaniaGemma 4 26B (control_model: answer)67/67 polecenia (z akcjami)2,7 s
Zgadywanie zdania po bełkocieGemma 4 26Bz 10 przykładów: 6 trafnych, 3× „nie wiem” (prośba o powtórzenie), 1 śmiała propozycja1,6 s
Wersja oszczędnaGemma 4 E4B we wszystkich rolach29/34 dane, 8/8 streszczeń, 26/32 polecenia0,8 s / 3,8 s / 1,7 s

Pełne porównanie kilkunastu modeli (Gemma 4, Bielik, PLLuM, Qwen 3.5, Ministral): trafność i czas w każdej roli, wnioski, typowe błędy i wyniki douczania. Test można powtórzyć na własnym sprzęcie: rozdział 12.

Pamięć modeli. Ollama ładuje model z kontekstem podanym w zapytaniu. Integracja używa stałego kontekstu: 4096 dla routera i 8192 dla modelu odpowiedzi. Jeśli agent zapasowy w integracji Ollama używa tego samego modelu co model odpowiedzi, ustaw mu też kontekst 8192. Inaczej Ollama będzie przeładowywać model przy każdym pytaniu.

4. Instalacja

  1. Skopiuj custom_components/assist_router do katalogu /config/custom_components/. Możesz też dodać repozytorium w HACS jako repozytorium niestandardowe typu „Integracja”.
  2. Zrestartuj Home Assistant.
  3. Ustawienia → Urządzenia i usługi → Dodaj integrację → Assist Router.
  4. Ustawienia → Asystenci głosowi: w każdym potoku zmień agenta rozmowy na „Assist Router” (encja conversation.assist_router) i wyłącz „Preferuj obsługę poleceń lokalnie”. Przy włączonej opcji HA rozpoznaje wbudowane intencje przed Assist Routerem: „Ogłoszenie, …” poszłoby syntezą, a „Kto to mówi?” i nagrywanie wzorca głosu nie miałyby pierwszeństwa. Assist Router i tak najpierw przekazuje każde zdanie wbudowanemu agentowi, więc nic się nie traci.
  5. Skopiuj router.example.yaml z repozytorium do /config/assist/router.yaml i dopasuj encje do swojego domu (opis pliku: rozdział 8).
  6. W opcjach integracji podaj adres Ollamy i wybierz modele (rozdział 5).
  7. Opcjonalnie: Speaker ID, żeby asystent wiedział, kto pyta (rozdział 7, „Kto pyta”). Assist Router odczytuje mówcę sam, wystarczy zmienić w potokach zamianę mowy na tekst.

5. Opcje integracji

Ustawienia → Urządzenia i usługi → Assist Router → Konfiguruj otwiera menu:

PozycjaCo robi
UstawieniaDwa kroki opisane niżej.
Plik routera (router.yaml)Edycja /config/assist/router.yaml wprost w HA. Przed zapisem plik jest sprawdzany (zły YAML albo błąd konfiguracji pokazuje komunikat i nic nie zapisuje), a poprzednia wersja zostaje jako router.yaml.bak. Router czyta zmiany od następnego pytania, bez restartu.
Przykładowe komendyPodgląd zdań, które działają w tym domu, i ich generowanie (patrz niżej).
Nierozpoznane komendyTabela ostatnich 30 wpisów dziennika (od najnowszych) i opcja „Wyczyść dziennik”; poprzednia treść zostaje jako unrecognized_commands.csv.bak.

Przykładowe komendy dla Twojego domu

Akcja assist_router.generate_examples albo pozycja menu „Przykładowe komendy” działa tak:

  1. Dla każdego tematu z router.yaml model odpowiedzi układa zdania z prawdziwymi nazwami: encjami tematu, urządzeniami i pokojami (sterowanie) oraz domownikami.
  2. Każde zdanie przechodzi przez prawdziwy router. Zostaje tylko wtedy, gdy trafi do swojego tematu.
  3. Polecenia sterowania przechodzą też rozbiór i plan polecenia, bez wykonania.

Wynik:

Generowanie trwa kilka minut, bo każde zdanie to osobne zapytanie do routera. Opcje akcji: per_topic (ile zdań na temat, domyślnie 5) i topics (tylko wybrane tematy). Temat bełkotu (unclear) jest pomijany.

Akcja assist_router.clear_log czyści dziennik nierozpoznanych. Sensor „Nierozpoznane komendy” pokazuje liczbę wpisów, a w atrybucie last ostatnie 20, co przydaje się do karty Markdown na dashboardzie.

Ustawienia, krok pierwszy:

OpcjaCo robi
Zapisuj nierozpoznane komendyDopisuje każde nierozpoznane zdanie do /config/assist/unrecognized_commands.csv i wysyła zdarzenie assist_router_unrecognized.
Agent zapasowyAgent rozmowy dla pytań, którym router nie dał danych (temat „inne”, błąd). Puste = komunikat „Powiedziałeś: …”.
Czas na odpowiedź agenta zapasowegoPo tym czasie słychać „Powiedziałeś: …”.
Adres OllamyNp. http://localhost:11434. Puste = router wyłączony.

Krok drugi pojawia się, gdy podasz adres Ollamy. Lista modeli jest pobierana z serwera:

OpcjaCo robi
Model routeraWybiera temat. Wystarczy mały i szybki model.
Model odpowiedziOpcjonalny, ale potrzebny do odpowiedzi z danych. Pisze odpowiedź z danych tematu i streszcza Wikipedię. Bez niego pytania z danymi trafiają do agenta zapasowego, a z Wikipedii czytany jest początek artykułu.
Czas na odpowiedź routera / modeluDomyślnie 8 s i 15 s. Ten sam limit routera dotyczy też wyszukiwania w Wikipedii.
Trzymaj model w pamięciMinuty od ostatniego pytania. -1 = zawsze w pamięci, najszybsza odpowiedź. Oba modele są ładowane przy starcie integracji.

6. Plik router.yaml

/config/assist/router.yaml opisuje Twój dom. Kod integracji jest wspólny dla wszystkich użytkowników. Zmiany w pliku działają od razu, bez restartu HA. Brak pliku wyłącza router. Poniżej szkielet z najważniejszymi polami; pełny przykład domu z kilkunastoma tematami jest w rozdziale 8, a to, jak kod zbiera i przycina dane, opisuje rozdział 7.

# Kto mieszka w domu - trafia do promptu routera i modelu odpowiedzi
household: "Domownicy: Tomek (tata), Ania (mama), dzieci: Ola, Kuba."

# Osoby w pytaniach: match = początki słów (regex), żeby złapać odmianę
people:
  ola:  {name: Ola,  match: "ola|oli|olą|olę|aleksandr"}
  kuba: {name: Kuba, match: "kub|jakub"}
  tomek: {name: Tomek, match: "tom|tat(a|y|o|ą)\\b"}
  # family: {parents: [tomek, ania], children: [ola, kuba], men: [tomek, kuba]}
  # speaker: identyfikator głosu w Assist Speaker ID (domyślnie klucz osoby, tu „ola”, „kuba”)
  # owns: [calendar.kalendarz_oli] - encje tej osoby; room: Pokój Oli - jej pokój

sentences: 4              # ile zdań w streszczeniu z Wikipedii
wikipedia_language: pl
only_exposed: false       # true = encje tematów tylko spośród udostępnionych Assistowi
control_model: answer     # kto rozbiera polecenia sterowania: router albo answer
confirm_entities:         # zawsze dopytaj (wzorce entity_id albo nazwy)
  - switch.kuchnia_lodowka_przelacznik
  - switch.*_enabled
music_players:            # „puść …”: pokój -> odtwarzacze, pierwszy dostępny
  Salon: [media_player.salon_homepod, media_player.salon_home_assistant_voice]

topics:
  lekcje:
    description: szkoła, plan lekcji, zajęcia dodatkowe, o której kończy lekcje
    examples: ["Na którą Ola idzie jutro do szkoły?"]
    details: [sensor.plan_lekcji]    # atrybuty encji, przycięte do osoby i dnia z pytania
    days: dzis                       # bez dnia w pytaniu - dziś
    drop: [teacher, classroom]       # pomijane klucze

  kalendarz:
    description: plany, wydarzenia, spotkania, urodziny
    examples: ["Co mamy w weekend?"]
    days: "14"                       # bez dnia w pytaniu - 14 dni od dziś
    fetch:                           # usługa z odpowiedzią
      - action: calendar.get_events
        entities: [calendar.rodzina, calendar.praca]
        data: {start_date_time: "{start}", end_date_time: "{end}"}
        merge: events                # jedna lista ze wszystkich kalendarzy, po kolei
        title: Wydarzenia w kalendarzach

  samochod:
    description: auto - paliwo, zasięg, przebieg, czy zamknięte, gdzie stoi
    examples: ["Ile paliwa ma auto?", "Czy zamknąłem samochód?"]
    entities: [sensor.moje_auto_*, binary_sensor.moje_auto_*, lock.moje_auto_*]

  swiatla:
    description: które światła są włączone
    entities: [light.*, "!light.ipad_*"]   # „!” = wykluczenie
    skip_states: ["off"]

  sterowanie:
    description: polecenia - włącz, wyłącz, ustaw, przyciemnij, rolety, cieplej, głośniej
    examples: ["Zrób jaśniej w salonie.", "Opuść rolety do połowy."]
    control: true

  wiedza:
    description: pytanie o wiedzę ogólną - kim był, co to jest, historia, geografia
    examples: ["Kim był Mikołaj Kopernik?"]
    search: wikipedia

  inne:
    description: wszystko inne - rozmowa, żart, polecenia
    examples: ["Opowiedz coś ciekawego."]

Pola tematu

PoleZnaczenie
descriptionKrótki opis dla routera: o czym są pytania z tego tematu. Opisy tematów nie powinny się pokrywać.
examples2–4 przykładowe pytania. Najbardziej pomagają przy sformułowaniach z pogranicza dwóch tematów.
entitiesWzorce entity_id (* = dowolny ciąg, |klasa1,klasa2 = tylko te rodzaje czujników, !wzorzec = wykluczenie). Ich stany trafiają do danych po polsku: okno „otwarte”, zamek „zamknięty”. Pomijane są unknown/unavailable, najwyżej 80 encji, w kolejności wzorców (ważniejsze pierwsze).
detailsWzorce encji, których atrybuty idą do modelu (np. plan lekcji, saldo kont, odjazdy tramwajów). Listy są przycinane do osoby z pytania (klucz child albo person) i do dnia (klucz date albo datetime); szum (html, awatary, kolory, wartości false) jest pomijany, czasy są lokalne i z dniem tygodnia.
fetchUsługi z odpowiedzią: action, entities, data ({start} i {end} = zakres dni z pytania), opcjonalnie merge (lista do połączenia z wszystkich encji), title i limit (najwięcej pozycji, domyślnie 40). Np. kalendarze (calendar.get_events) albo prognoza (weather.get_forecasts).
daysDzień, gdy pytanie go nie podaje: dzis, jutro, tydzien albo liczba dni od dziś ("14"). Bez tego pola dane nie są filtrowane po dniu.
dropKlucze atrybutów pomijane w tym temacie.
personaltrue: temat dla osoby, która pyta, także bez „mam” i „moje” („Dzień dobry”). Rodzic (family.parents) dostaje swoje dane i dane dzieci, a dziecko tylko swoje. Mówca nieznany oznacza dane wszystkich. Imię w pytaniu zawsze wygrywa.
answer_hintDodatkowa instrukcja dla modelu odpowiedzi w tym temacie, np. jak ułożyć podsumowanie dnia i co powiedzieć rodzicowi, a co dziecku.
verbatimtrue: odpowiedź HA na wzorcowe zdanie z command jest czytana dosłownie, bez modelu odpowiedzi. Kilka zdań oddzielonych ; daje wszystkie odpowiedzi po kolei. Przydaje się, gdy automatyzacja ma stały układ odpowiedzi, np. wiadomości dnia: „Dzisiejsze wiadomości z Polski: Pierwsza wiadomość…”.
answer_hintsInstrukcje zależne od tego, kto pyta. Klucze, po kolei: osoba (np. ola), girl/boy (dzieci), child/parent, unknown (mówca nierozpoznany). Brak pasującego klucza oznacza answer_hint. Prosta instrukcja dla jednej osoby działa pewniej niż jedna długa z warunkami.
answer_tokensDłuższa odpowiedź niż domyślne 160 tokenów, np. 360 dla podsumowania dnia.
command(starsze) Szablon HA (Jinja) z wzorcowym pytaniem do Twojego HA, np. zdaniem automatyzacji. Kilka pytań rozdziel znakiem ;. Zamiast tego lepiej użyć details i fetch - bez osobnych automatyzacji Assist.
search: wikipediaTemat wiedzy: hasło jest brane z pytania, artykuł streszczany.
control: trueTemat poleceń sterowania (rozdział 10).
unclear: trueTemat bełkotu z rozpoznawania mowy: model odgaduje, co powiedziano, a asystent pyta „Czy chodziło o…?”; bez propozycji prosi o powtórzenie (rozdział 10).
brak danych i searchPytanie idzie do agenta zapasowego.

Zmienne w szablonie command (starsze pole)

ZmiennaWartość
osobapierwsze imię z pytania (forma z people.name) albo brak
osobywszystkie imiona z pytania, po kolei: „Ola i Kuba” → ['Ola', 'Kuba']
ktoklucz pierwszej osoby z people (np. ola)
kiedydzis, jutro, pojutrze, weekend, tydzien, dzień tygodnia (poniedzialek … niedziela) albo brak
tekstpełne pytanie, np. do wyciągnięcia numeru linii tramwaju

Prompty modelu odpowiedzi

Domyślne prompty są wbudowane w integrację. Możesz je nadpisać w router.yaml polami answer_prompt (odpowiedź z danych) i summary_prompt (streszczenie). W tekście możesz użyć pól {household}, {now} (data i godzina po polsku) i {sentences}.

7. Skąd router bierze dane

Router nie potrzebuje specjalnych automatyzacji ani zdań Assist. Dane bierze wprost z Home Assistanta, a w router.yaml opisujesz tylko, które encje i usługi należą do tematu. Resztą zajmuje się kod: wybiera z danych to, o co pytano, usuwa szum i podaje modelowi odpowiedzi zwięzłą listę faktów.

Pytanie „Na którą Ola idzie jutro do szkoły?” Router (model) tylko temat: szkola Kod osoba: ola · dzień: jutro Temat w router.yaml szkola: details, fetch, entities, days, drop entities stany encji po polsku „Okno salon: otwarte (od 12 min)” details atrybuty encji plan lekcji, saldo, odjazdy, adres fetch usługi, które zwracają dane calendar.get_events, weather.get_forecasts Przycinanie (kod, bez modelu) osoba: tylko elementy z child/person pasującym do „Ola” · dzień: tylko date/datetime z jutra bez szumu (html, awatary, kolory, false) · czas lokalny i dzień tygodnia · limity długości Dane dla modelu (zwięzły YAML) zwykle kilkaset do kilku tysięcy znaków Model odpowiedzi „Ola idzie jutro na 8:00, pierwsza jest matematyka.”

Trzy źródła danych

Pole tematuCo trafia do modeluTypowe przykłady
entitiesStan każdej pasującej encji, po polsku, z najważniejszymi atrybutami: jasność światła, pozycja rolety, temperatura termostatu, co gra, od kiedy czujnik jest otwarty.okna i drzwi, światła, czujniki w pokojach, auto, osoby (person.*), ruch, alarm, pralka
detailsAtrybuty encji, czyli dane, których nie widać w samym stanie: listy, plany, salda, adresy.plan lekcji z dziennika, odjazdy z przystanku, adres telefonu (*_geocoded_location), konta kieszonkowego
fetchOdpowiedź usługi HA, która zwraca dane. Zakres dni wynika z pytania.kalendarze (calendar.get_events), prognoza (weather.get_forecasts)

W jednym temacie możesz łączyć wszystkie trzy. Pogoda w przykładzie bierze bieżący odczyt z czujników (entities) i prognozę z usługi (fetch).

Jak kod przycina dane

Dzień z pytania

W pytaniuWartośćZakres dni
„dziś”, „dzisiaj”, „teraz”dzisdziś
„jutro”, „pojutrze”jutro, pojutrzeten jeden dzień
„w poniedziałek” … „w niedzielę”poniedzialek … niedzielanajbliższy taki dzień (dziś, jeśli to dziś)
„w weekend”weekendnajbliższa sobota i niedziela
„w tym tygodniu”tydziendziś i 6 kolejnych dni
nicdays tematunp. dzis albo "30" (dziś i 29 kolejnych dni)

Przykład krok po kroku: plan lekcji

Pytanie: „Na którą Ola idzie jutro do szkoły?”. Router wybiera temat szkola, a kod wyciąga osobę ola i dzień jutro. Temat ma details: [sensor.dziennik_plan_lekcji]. Atrybuty tej encji to cały tydzień obojga dzieci, kilkanaście tysięcy znaków:

{"friendly_name": "Plan lekcji", "week_start": "2026-10-19",
 "children": [
   {"child": "Aleksandra", "avatar": "https://…", "color": "#f5ec00",
    "days": [
      {"date": "2026-10-22", "lessons": [{"start": "08:00", "subject": "Matematyka", "canceled": false}, …]},
      {"date": "2026-10-23", "lessons": [{"start": "08:00", "subject": "Matematyka", "canceled": false},
                                         {"start": "08:55", "subject": "Przyroda", "canceled": true}, …]},
      …]},
   {"child": "Jakub", "days": […]}]}

Po przycięciu model dostaje tylko jutro Oli, bez awatarów, kolorów i canceled: false:

Plan lekcji (stan: 2026-10-22):
week_start: 2026-10-19 (poniedziałek)
children:
- child: Aleksandra
  days:
  - date: 2026-10-23 (piątek)
    lessons:
    - start: 08:00
      subject: Matematyka
    - start: 08:55
      subject: Przyroda
      canceled: true

Odpowiedź modelu: „Ola idzie jutro na 8:00, pierwsza jest matematyka. Przyroda o 8:55 jest odwołana.” Model sam zauważył odwołaną lekcję, bo flaga true została w danych.

Przykład: kilka kalendarzy naraz

calendar.rodzina calendar.praca_tomka calendar.urodziny calendar.swieta_w_polsce calendar.get_events „w piątek” → start: piątek 00:00 end: sobota 00:00 merge: events 08:00 (piątek) Rodzina: wywiadówka 11:45 (piątek) Praca: spotkanie z klientem 16:30 (piątek) Rodzina: urodziny Franka jedna lista po godzinie, ze źródłem

Bez merge każdy kalendarz byłby osobnym źródłem i model musiałby sam układać wydarzenia w kolejności. Z merge pytanie „Co mamy w piątek?” dostaje gotowy plan dnia, a „Kiedy są urodziny babci?” przeszukuje 30 dni (days: "30").

Bezpieczeństwo fetch. Integracja wywołuje tylko usługi, które wyłącznie zwracają dane (w HA: SupportsResponse.ONLY, np. calendar.get_events, weather.get_forecasts, todo.get_items). Wpis z usługą, która coś robi (lock.unlock, skrypt), jest pomijany z ostrzeżeniem w logu. Pytanie nigdy niczego nie przełączy.

A automatyzacje ze zdaniami?

Nie są potrzebne. Jeśli już je masz, mogą zostać: HA sprawdza je przed routerem, więc zdanie dokładnie pasujące do automatyzacji dostaje odpowiedź w ułamku sekundy, bez modelu. Każde inne sformułowanie trafia do routera i odpowiada na nie model z danych. Starsze pole command (wzorcowe pytanie do automatyzacji) nadal działa, ale nowe tematy lepiej opisywać przez entities, details i fetch.

Kto pyta: rozpoznany mówca (Assist Speaker ID)

Z integracją Speaker ID (zamiana mowy na tekst, która rozpoznaje też głos) router wie, kto zadał pytanie. Wystarczy w pipeline'ach ustawić jej STT („Mowa na tekst z mówcą”). Agentem zostaje Polski Assist, który sam odczytuje mówcę z tego samego przebiegu pipeline'u.

Encje i pokój osoby (owns, room):

Podsumowanie dnia i „dobranoc” dla osoby: tematy z personal: true zbierają dane mówcy. „Dzień dobry”, „Jak wygląda mój dzień?”, „Co mnie dziś czeka?” od dziecka dają jego lekcje, sprawdziany, obowiązki i kalendarz. Od rodzica dają jego kalendarz i pogodę oraz krótki skrót o dzieciach. Jak ułożyć odpowiedź, mówi answer_hint tematu. Przykład: tematy podsumowanie_dnia i dobranoc w router.example.yaml. Sceny o tych nazwach uruchamia się tylko jawnie („Uruchom scenę Dzień dobry”). Samo „Dzień dobry” jest powitaniem.

Ogłoszenia (intercom): zdanie zaczynające się od „ogłoszenie”, „komunikat”, „do wszystkich” albo „intercom” („Ogłoszenie, dzieci proszę przyjść na obiad”) nie trafia do HA ani do modeli. Assist Speaker ID odtwarza nagranie mówcy, bez cięcia i bez zmian, na pozostałych głośnikach, a głośnik, który nagrał, mówi tylko „Ogłoszenie wysłane.”. Odtwarzacze dodatkowe, np. HomePody, podajesz w router.yaml jako announce_players: [media_player.…]. Bez nagrania (tekst wpisany ręcznie albo brak Assist Speaker ID) zdanie idzie dalej zwykłą drogą: HA czyta je syntezą mowy. Te słowa muszą być na początku zdania, więc „Jakie są ogłoszenia w Librusie?” to zwykłe pytanie.

Sprawdzenie rozpoznawania: „Kto to mówi?”, „Kim jestem?”, „Jak mnie rozpoznajesz?”, „Co to za głos?”. Asystent odpowiada od razu, bez HA i modeli, z wyniku tej samej wypowiedzi, np. „Mówi Ola, podobieństwo 82 procent. Następna osoba: Kuba, 35 procent.” Odpowiedź może też brzmieć „nie rozpoznaję tego głosu” albo „nie jestem pewien”, z dwoma najbardziej podobnymi głosami. Krótkie pytanie (poniżej 1 s mowy) daje prośbę o dłuższe zdanie, np. „Powiedz mi, kto teraz do ciebie mówi”.

8. Przykładowy router.yaml

Plik router.example.yaml w repozytorium opisuje zmyślony dom: Tomek, Ania i dwoje dzieci, Ola i Kuba. Encje mają nazwy, jakie zwykle tworzą popularne integracje: aplikacja mobilna HA (lokalizacja telefonów), Frigate (kamery), Met.no (pogoda), kalendarze, auto, odkurzacz, pralka i listy zakupów. Wystarczy podmienić nazwy encji na swoje (Narzędzia deweloperskie → Stany).

TematSkąd daneCo pokazuje
kalendarzfetch: calendar.get_events, mergekilka kalendarzy w jednej liście, dzień z pytania, bez dnia 30 dni
szkoladetails albo fetchplan lekcji przycięty do dziecka i dnia; dwa warianty dziennika
pogodaentities + 2 × fetchodczyt z czujnika i prognoza dzienna oraz godzinowa (limit: 24)
czujnikientities z |klasaczujniki według device_class, tylko udostępnione
okna_drzwientitiesokna, drzwi, garaż i rolety z czasem „od kiedy”
osobyentities + detailsdomownicy, kamery Frigate, ruch, adres z telefonu; kolejność wzorców
samochodentitieswszystkie czujniki auta - model wybiera
swiatlaentities, skip_states, !tylko świecące, bez ekranu tabletu
mediaentities, skip_statestylko odtwarzacze, które coś grają
listyentities: [todo.*]pozycje list (integracja sama woła todo.get_items)
komunikacjadetailsodjazdy z przystanków z atrybutów
niezrozumialeunclear: truebełkot z rozpoznawania mowy: propozycja „Czy chodziło o…?” albo prośba o powtórzenie
sterowanie, wiedza, inne-polecenia, Wikipedia, agent zapasowy
# Przykładowy router.yaml dla Assist Routera - skopiuj do /config/assist/router.yaml
# i dopasuj encje do swojego domu (Narzędzia deweloperskie → Stany).
#
# Dom z przykładu: Tomek (tata), Ania (mama), dzieci Ola i Kuba. Encje mają nazwy, jakie
# zwykle tworzą popularne integracje: aplikacja mobilna HA (lokalizacja telefonów),
# Frigate (kamery), Met.no (pogoda), kalendarze Google/CalDAV, integracja auta, odkurzacza
# i pralki. Wszystkie nazwy encji są przykładowe.
#
# Jak router używa tego pliku:
#   1. Model routera wybiera JEDEN temat z `topics` (na podstawie description i examples).
#   2. Kod wyciąga z pytania osoby (`people`) i dzień („jutro”, „w piątek”).
#   3. Temat zbiera dane: stany encji (entities), atrybuty encji (details) i odpowiedzi
#      usług (fetch) - przycięte do osoby i dnia z pytania.
#   4. Model odpowiedzi odpowiada na pytanie wyłącznie z tych danych.
# Zmiany w pliku działają od razu, bez restartu HA.

# --- Ustawienia ogólne ---------------------------------------------------------------

# Kto mieszka w domu - trafia do promptów (model wie, że „mama” to Ania, a „dzieci” to
# Ola i Kuba).
household: >-
  Domownicy: Tomek (tata, najczęściej pyta), Ania (mama), dzieci: Ola (Aleksandra, 10 lat)
  i Kuba (Jakub, 7 lat).

# Osoby w pytaniach. `match` to początki słów (regex), żeby złapać odmianę: „Oli”, „Olą”,
# „Kubie”, „taty”. Osoba z pytania przycina dane, które mają klucz `child` albo `person`
# (np. plan lekcji tylko Oli). Pytanie bez imienia („dzieci”) - dane wszystkich.
# Z rozpoznawaniem mówcy (integracja Assist Speaker ID) pytanie w 1. osobie („Co mam jutro?”)
# dotyczy tego, kto mówi. Dodatkowe pola osoby:
#   speaker - identyfikator jej głosu w Assist Speaker ID (domyślnie klucz osoby, np. „ola”),
#   owns    - jej encje (np. kalendarz pracy, telefon): w pytaniu o inną osobę są pomijane,
#             encje niczyje (kalendarz rodziny) - zawsze,
#   room    - jej pokój: „zgaś światło w moim pokoju” od tej osoby = ten obszar.
people:
  tomek:
    name: Tomek
    match: "tom|tat(a|y|o|ą|ę)\\b"
    owns: [calendar.praca_tomka, sensor.telefon_tomka_*]
  ania:
    name: Ania
    match: "ani|anna|anny|mam(a|y|ie|ą|ę)\\b"
    owns: [sensor.telefon_ani_*]
  ola:
    name: Ola
    match: "ol(a|i|ą|ę|u)\\b|olk|aleksandr"
    room: Pokój Oli
  kuba:
    name: Kuba
    match: "kub|jakub"
    room: Pokój Kuby

# Określenia rodzinne względem mówcy: „mój brat” od Oli = Kuba, „moja córka” od Tomka =
# Ola, „moje dzieci” od Ani = Ola i Kuba. „mama”/„tata” to stałe imiona (match wyżej).
family:
  parents: [tomek, ania]
  children: [ola, kuba]
  men: [tomek, kuba]

sentences: 4                # ile zdań w streszczeniu z Wikipedii
wikipedia_language: pl
only_exposed: false         # true = encje tematów tylko spośród udostępnionych Assistowi
control_model: answer       # rozbiór poleceń sterowania: router albo answer (większy model)

# Sterowanie: przy tych urządzeniach asystent zawsze pyta „Czy wyłączyć…?” - także gdy
# trafią do polecenia zbiorczego („wyłącz wszystkie przełączniki”).
confirm_entities:
  - switch.lodowka
  - switch.serwer_*
  - switch.*_kamera_enabled     # włączniki kamer (Frigate)
  - Router Wi-Fi                # można też podać nazwę urządzenia

# „Puść …”: pokój -> odtwarzacze w kolejności (pierwszy dostępny). Odtwarzacze Music
# Assistanta często nie mają pokoju - stąd ta lista. Bez pokoju w poleceniu: pokój
# głośnika, do którego mówisz.
music_players:
  Salon: [media_player.salon_glosnik, media_player.salon_telewizor]
  Sypialnia: [media_player.sypialnia_glosnik]
  Kuchnia: [media_player.kuchnia_glosnik]

# --- Tematy ---------------------------------------------------------------------------
#
# Źródła danych tematu (można łączyć):
#   entities  - wzorce entity_id; do modelu idzie stan po polsku („otwarte”, „w domu”).
#               `*` = dowolny ciąg, `|klasa1,klasa2` = tylko te device_class,
#               `!wzorzec` = wykluczenie. Kolejność ma znaczenie (limit 80 encji).
#   details   - wzorce encji, których ATRYBUTY idą do modelu (listy, plany, salda).
#   fetch     - usługi, które zwracają dane (calendar.get_events, weather.get_forecasts…).
#               `{start}` i `{end}` = zakres dni z pytania.
#   days      - dzień, gdy pytanie go nie podaje: dzis, jutro, tydzien albo liczba dni.
#   drop      - klucze atrybutów do pominięcia (szum, którego model nie potrzebuje).
#   skip_states - stany pomijane (np. wyłączone światła - zostają tylko świecące).

topics:
  # Podsumowania dla osoby, która pyta (wymaga Assist Speaker ID; bez niego - dla całej rodziny).
  # personal: bez imienia w pytaniu dane mówcy, a rodzica także dzieci.
  podsumowanie_dnia:
    description: dzień dobry, podsumowanie dnia, jak wygląda mój dzień, co mnie dziś czeka, plan na resztę dnia
    examples: ["Dzień dobry", "Jak wygląda mój dzisiejszy dzień?", "Co mnie dzisiaj czeka?"]
    personal: true
    days: dzis
    answer_tokens: 360
    answer_hint: >-
      To podsumowanie dnia na głos. Mów do osoby z „Pyta:”, po imieniu, zwykłymi zdaniami.
      Najpierw pogoda, potem jej plan (lekcje, wydarzenia, obowiązki). Rodzicowi dodaj
      jedno lub dwa zdania o najważniejszych sprawach dzieci. Do 6 zdań.
    details: [sensor.plan_lekcji, sensor.obowiazki]
    fetch:
      - action: calendar.get_events
        entities: [calendar.rodzina, calendar.praca_tomka]
        data: {start_date_time: "{start}", end_date_time: "{end}"}
        merge: events
      - action: weather.get_forecasts
        entities: [weather.dom]
        data: {type: daily}

  dobranoc:
    description: dobranoc, idę spać, co mnie czeka jutro
    examples: ["Dobranoc", "Idę spać", "Co mnie jutro czeka?"]
    personal: true
    days: jutro
    # instrukcja zależna od mówcy: osoba, girl/boy, child/parent, unknown
    answer_hints:
      girl: >-
        Jednym-dwoma zdaniami powiedz, co ją czeka jutro (bez domu i pogody), potem czule,
        bajkowo życz dobrej nocy po imieniu, jak księżniczce.
      boy: >-
        Jednym-dwoma zdaniami powiedz, co go czeka jutro (bez domu i pogody), potem na wesoło
        życz dobrej nocy po imieniu (np. ładowanie baterii superbohatera).
      parent: >-
        Co go czeka jutro i pogoda rano, co w domu zostało otwarte lub włączone, jedno
        zdanie o jutrze dzieci. Zakończ „Dobranoc”. Do 5 zdań.
      unknown: >-
        Krótko dla całej rodziny, bez imion: co jutro i co otwarte lub włączone w domu.
    details: [sensor.plan_lekcji]
    fetch:
      - action: calendar.get_events
        entities: [calendar.rodzina, calendar.praca_tomka]
        data: {start_date_time: "{start}", end_date_time: "{end}"}
        merge: events
    entities: [binary_sensor.*_okno, light.*]


  # Kalendarze: wydarzenia ze wszystkich kalendarzy w jednej liście, po kolei.
  # „Co mamy w piątek?” -> tylko piątek; „Kiedy są urodziny babci?” -> 30 dni od dziś.
  kalendarz:
    description: plany, wydarzenia, spotkania, urodziny, imieniny, święta, co mamy zaplanowane, terminy
    examples:
      - Co mamy w weekend?
      - Ile mam jutro spotkań w pracy?
      - Kiedy są urodziny babci?
      - Czy w czwartek coś się dzieje?
    days: "30"
    drop: [description]           # opisy spotkań (linki do wideorozmów) tylko zajmują miejsce
    fetch:
      - action: calendar.get_events
        entities: [calendar.rodzina, calendar.praca_tomka, calendar.urodziny, calendar.swieta_w_polsce]
        data: {start_date_time: "{start}", end_date_time: "{end}"}
        merge: events             # jedna lista ze wszystkich kalendarzy, posortowana po godzinie
        title: Wydarzenia w kalendarzach (po kolei)
        limit: 60

  # Szkoła z dziennika elektronicznego. Dwa typowe warianty - wybierz pasujący:
  #  a) integracja daje encję z atrybutami (dzieci -> dni -> lekcje): `details`;
  #  b) integracja daje kalendarz lekcji dla każdego dziecka: `fetch` jak w kalendarzu.
  szkola:
    description: szkoła, plan lekcji, o której zaczyna lub kończy lekcje, kiedy wraca ze szkoły, zastępstwa, odwołane lekcje
    examples:
      - Na którą Ola idzie jutro do szkoły?
      - Czy Kuba ma jutro WF?
      - O której dzieci kończą dziś lekcje?
    details: [sensor.dziennik_plan_lekcji]      # wariant a)
    days: dzis                                  # bez dnia w pytaniu - dziś
    drop: [teacher_id, classroom_id]
    # fetch:                                    # wariant b)
    #   - action: calendar.get_events
    #     entities: [calendar.lekcje_ola, calendar.lekcje_kuba]
    #     data: {start_date_time: "{start}", end_date_time: "{end}"}
    #     merge: events

  # Pogoda: teraz z czujników na balkonie, prognoza z usługi (dzienna i godzinowa).
  pogoda:
    description: pogoda na zewnątrz teraz i prognoza, deszcz, wiatr, czy wziąć parasol, jak się ubrać, temperatura na dworze
    examples:
      - Czy jutro będzie padać?
      - Czy wziąć parasol?
      - Jak się ubrać na weekend?
      - Ile jest stopni na dworze?
    entities:
      - sensor.balkon_temperatura
      - sensor.balkon_wilgotnosc
      - sensor.deszczomierz_*
    drop: [wind_bearing, uv_index, cloud_coverage, dew_point, pressure]
    fetch:
      - action: weather.get_forecasts
        entities: [weather.dom]
        data: {type: daily}
        title: Prognoza na kolejne dni
      - action: weather.get_forecasts
        entities: [weather.dom]
        data: {type: hourly}
        title: Prognoza godzinowa
        limit: 24                 # najwyżej doba - więcej tylko spowalnia odpowiedź

  # Klimat w pokojach: czujniki według rodzaju (device_class) i termostaty.
  czujniki:
    description: temperatura, wilgotność, CO2, jakość powietrza w pokojach, ogrzewanie, termostaty, czy jest duszno
    examples:
      - Ile stopni jest w pokoju Oli?
      - Gdzie jest najwyższe CO2?
      - Na ile jest ustawione ogrzewanie w salonie?
    only_exposed: true            # tylko czujniki udostępnione Assistowi (bez technicznych)
    entities:
      - sensor.*|temperature,humidity,carbon_dioxide,pm25,volatile_organic_compounds
      - climate.*

  # Okna, drzwi i rolety: czujniki otwarcia i pozycja rolet.
  okna_drzwi:
    description: które okna lub drzwi są otwarte, czy wszystko zamknięte, drzwi wejściowe, balkon, garaż, rolety
    examples:
      - Które okna są otwarte?
      - Czy zamknąłem drzwi balkonowe?
      - Od kiedy jest otwarte okno w sypialni?
    entities:
      - binary_sensor.*|door,window,opening,garage_door
      - cover.*

  # Kto jest w domu i gdzie: domownicy (person), adres z telefonu (aplikacja mobilna HA),
  # kamery Frigate (liczba osób), czujniki ruchu. Kolejność: najważniejsze najpierw.
  osoby:
    description: kto jest w domu, gdzie ktoś jest, czy ktoś jest w pokoju, gdzie był ruch, ile osób widzą kamery
    examples:
      - Gdzie jest mama?
      - Czy Kuba jest już w domu?
      - Czy ktoś jest w salonie?
      - Ile osób widzi kamera w ogrodzie?
    entities:
      - person.*
      - sensor.*_person_count                    # Frigate: liczba osób w kadrze
      - binary_sensor.*|motion,occupancy,presence
    details: [sensor.telefon_*_geocoded_location] # adres telefonu (atrybuty: ulica, miasto)
    drop: [Location, Country, ISO Country Code, Postal Code, Time Zone, Areas Of Interest,
           Administrative Area, Sub Administrative Area, Ocean, Inland Water]

  # Samochód: wszystkie czujniki auta - model wybierze, o co pytano.
  samochod:
    description: samochód - paliwo, zasięg, przebieg, spalanie, serwis, czy zamknięty, okna, opony, gdzie stoi
    examples:
      - Czy muszę zatankować?
      - Czy zamknąłem auto?
      - Gdzie zaparkowałem?
      - Kiedy przegląd?
    entities:
      - sensor.moje_auto_*
      - binary_sensor.moje_auto_*
      - lock.moje_auto_*
      - device_tracker.moje_auto_*

  # Światła: tylko świecące (skip_states), bez ekranu tabletu i podświetleń głośników.
  swiatla:
    description: które światła są włączone, gdzie się świeci, czy zostało zapalone światło, jasność lamp
    examples:
      - Czy zostawiłem gdzieś zapalone światło?
      - Które lampy świecą w kuchni?
    only_exposed: true
    entities:
      - light.*
      - "!light.*_ekran"
      - "!light.*_led_ring"
    skip_states: ["off"]

  # Co gra: tylko odtwarzacze, które coś robią.
  media:
    description: co teraz gra, muzyka, telewizor, głośniki, jaka głośność, czy telewizor jest włączony
    examples:
      - Co teraz gra w salonie?
      - Czy telewizor jest włączony?
    only_exposed: true
    entities:
      - media_player.*
    skip_states: ["off", "idle", "standby"]

  # Odkurzacz, pralka - stan i czas do końca.
  odkurzacz:
    description: odkurzacz, robot sprzątający, czy skończył, bateria, filtr, szczotki
    examples: ["Czy odkurzacz skończył?", "Ile baterii ma robot?"]
    entities: [vacuum.*, sensor.odkurzacz_*]

  pranie:
    description: pralka, suszarka, czy pranie się skończyło, ile zostało do końca
    examples: ["Czy pralka skończyła?", "Ile jeszcze będzie suszyć?"]
    entities: [sensor.pralka_*, sensor.suszarka_*]

  # Listy (todo): zawartość list, nie tylko liczba pozycji (todo.get_items).
  listy:
    description: lista zakupów, co trzeba kupić, przypomnienia, rzeczy do zrobienia
    examples: ["Co jest na liście zakupów?", "Czy mamy na liście mleko?"]
    entities: [todo.*]

  # Alarm i bezpieczeństwo - tylko odczyt (włączanie alarmu zostaw automatyzacjom).
  alarm:
    description: alarm, czy alarm jest włączony, tryb ochrony, zalanie, dym, gaz, czy gdzieś cieknie woda
    examples: ["Czy alarm jest włączony?", "Czy czujnik dymu coś wykrył?"]
    entities:
      - alarm_control_panel.*
      - binary_sensor.*|moisture,smoke,gas,carbon_monoxide

  # Komunikacja miejska: odjazdy z przystanków (integracje MPK/ZTM mają listę odjazdów
  # w atrybutach). Bez dnia - wszystko, co jest w danych.
  komunikacja:
    description: tramwaj, autobus, odjazdy z przystanku, za ile jest tramwaj
    examples: ["Za ile mam tramwaj 8?", "Kiedy jedzie autobus z Parkowej?"]
    details: [sensor.przystanek_*]
    drop: [html, original_departure]

  # Stan pozostałych urządzeń (gniazdka, przełączniki).
  urzadzenia:
    description: czy urządzenie jest włączone - gniazdka, przełączniki, ładowarki, akwarium
    examples: ["Czy akwarium jest włączone?", "Czy ładowarka w garażu działa?"]
    only_exposed: true
    entities: [switch.*]

  # Polecenia, których HA nie rozpoznał: rozbiór na intencję HA i wykonanie
  # (z dopytaniem przy wątpliwościach). Szczegóły: readme, rozdział „Sterowanie domem”.
  sterowanie:
    description: polecenia - włącz, wyłącz, zgaś, zapal, ustaw, przyciemnij, kolor światła, rolety, cieplej, głośniej, puść muzykę, odkurz, dopisz do listy
    examples:
      - Zrób ciepłe światło w sypialni.
      - Opuść rolety do połowy.
      - Puść jazz w kuchni.
      - Dopisz mleko do listy zakupów.
    control: true

  # Wiedza ogólna: streszczenie artykułu z Wikipedii (3–4 zdania).
  wiedza:
    description: pytanie o wiedzę ogólną - kim był, co to jest, historia, geografia, nauka
    examples: ["Kim był Mikołaj Kopernik?", "Co to jest fotosynteza?"]
    search: wikipedia

  # Bełkot z rozpoznawania mowy: słowa do siebie nie pasują. Asystent zgaduje, co powiedziano
  # („Czy chodziło o: włącz nastrój w salonie?”) albo prosi o powtórzenie. W opisie napisz
  # wprost, co tu NIE należy - inaczej router wrzuci tu też dziwne, ale sensowne pytania.
  niezrozumiale:
    description: zdanie bez sensu - słowa do siebie nie pasują i nie da się z nich zrozumieć ani pytania, ani polecenia; wygląda na przekręcone przez rozpoznawanie mowy. Tu NIE należą pytania i polecenia z jednym przekręconym słowem, gdy sens jest jasny, ani pytania o wiedzę, rozmowa i żarty
    examples:
      - Głup nastrój w pisarowie.
      - Szuflada wieczór gazeta kuchni.
    unclear: true

  # Wszystko inne - agent zapasowy (rozmowa, żarty).
  inne:
    description: wszystko inne - rozmowa, żart, prośby, pytania bez danych w domu
    examples: ["Opowiedz coś ciekawego.", "Jak się masz?"]

9. Jak dodać nowy temat (krok po kroku)

Przykład: pytania o pralkę i suszarkę, np. „Ile jeszcze będzie prać?” albo „Czy mogę wyjąć pranie?”.

  1. Znajdź encje. Narzędzia deweloperskie → Stany, wyszukaj np. pralka. Zwróć uwagę, czy potrzebna informacja jest w stanie (np. „czas do końca: 25 min”) - wtedy wystarczy entities, czy w atrybutach (listy, plany) - wtedy details.
  2. Jeśli dane daje usługa (kalendarz, prognoza), sprawdź ją w Narzędzia deweloperskie → Akcje z zaznaczonym „Zwróć odpowiedź”. Ta sama akcja z tymi samymi danymi trafia do fetch; zamiast dat wpisz {start} i {end}.
  3. Dopisz temat:
      pranie:
        description: pralka, suszarka, czy pranie się skończyło, ile jeszcze potrwa
        examples: ["Czy suszarka skończyła?", "Ile jeszcze będzie prać?"]
        entities: [sensor.pralka_*, sensor.suszarka_*]
  4. Zapisz i zapytaj swobodnie. W pliku CSV, w kolumnie route, zobaczysz np. pranie + 12 encji + odpowiedź, a dla details i fetch szkola + 1 źródeł + odpowiedź.
  5. Jeśli router wybiera zły temat, popraw description i dodaj pytanie z dziennika do examples.
  6. Jeśli odpowiedź jest zła, a temat dobry, zobacz, co model dostał: dane są za długie (dodaj drop, days, limit), brakuje w nich odpowiedzi (dodaj encję) albo zawierają podobne, mylące encje (wyklucz je !wzorzec). Pomaga tools/compare_sources.py (rozdział 12).
Dobre praktyki

10. Sterowanie domem

Temat z control: true obsługuje polecenia, których HA nie rozpoznał. Zdania hassil i automatyzacje zawsze mają pierwszeństwo.

  1. Poprawki Whispera. Pewne poprawki są stosowane od razu („czystkę” → „wszystkie”), a niepewne (np. „jaś” → „zgaś”) oznaczają dopytanie. Listę zawiera WHISPER_FIXES w control.py.
  2. Rozbiór. Model zwraca JSON wymuszony schematem. Pole intencja to intencja HA: HassTurnOn/Off, HassLightSet, HassSetPosition, HassClimateSetTemperature, HassSetVolume/Relative, sterowanie mediami, HassMediaSearchAndPlay („puść …”), odkurzacz lub HassListAddItem. Pola domena, obszar i pietro to wybór z list Twojego HA. Do tego dochodzą nazwa (fragment wypowiedzi), wszystkie, wartosc i zmiana (więcej / mniej), a dla światła kolor (np. czerwony) i barwa (ciepła 2700 K, neutralna 4000 K, zimna 6500 K), dla muzyki szukaj i rodzaj (utwór, album, wykonawca, playlista, gatunek, podcast).
  3. Dopasowanie w kodzie.
    • Fragment nazwy jest porównywany z nazwami i aliasami encji udostępnionych Assistowi. Pod uwagę idą rdzenie słów, więc odmiana nie przeszkadza.
    • Gdy fragment jest słaby, wygrywa urządzenie, którego wszystkie słowa nazwy padły w poleceniu.
    • Obszar, którego nie było w wypowiedzi, jest pomijany. Model potrafi pomylić „garderobie” z „Garaż”.
    • Zmiany względne („jaśniej”, „wyżej”, „cieplej”) są liczone z bieżącego stanu: o 25% albo o 1°.
  4. Dopytanie („Czy wyłączyć Oświetlenie garderoby?”, rozmowa zostaje otwarta przez 30 s) następuje, gdy:
    • poprawka Whispera była niepewna;
    • w poleceniu nie ma żadnego znanego czasownika;
    • pasuje kilka urządzeń (ale nie encje-bliźniaki - patrz niżej);
    • chodzi o zamek, bramę albo drzwi garażowe;
    • urządzenie jest na liście confirm_entities (także w poleceniu zbiorczym, np. „wyłącz wszystkie przełączniki”).
    Przy kilku podobnie pasujących urządzeniach („roletą wewnętrzną okno” - mała i duża) asystent pyta, zamiast podmieniać je na inne urządzenie, którego słowa akurat padły w poleceniu (roleta zewnętrzna okna). Wygrywa tylko urządzenie z większą liczbą słów z polecenia („lampę w garderobie” → Garderoba Lampa). Pytanie ma dwie formy. Przy niepewności („Czy dobrze rozumiem, że mam uruchomić Ekspres - zrób kawę?”) samo „nie” znaczy „źle zrozumiałeś”: asystent przeprasza, prosi o powtórzenie i słucha dalej, a „anuluj”, „nie trzeba” albo „koniec” kończą rozmowę. Przy urządzeniach wrażliwych („Czy otworzyć drzwi wejściowe?”) „nie” znaczy po prostu „nic nie rób”. Odpowiedzi działają tak samo przy każdym pytaniu asystenta: zgoda – „tak”, „dokładnie (tak)”, „o to (mi) chodzi”, „działaj”, „zrób”, „wykonaj”, „dawaj”; powtórzenie – „nie”, „o coś innego”, „nie o to chodzi”, „jeszcze raz”, „(może) powtórzę” – asystent przeprasza i słucha dalej; koniec – „anuluj”, „koniec”, „nieważne”, „zostaw (to)”, „nic nie rób”, „daj (sobie z tym) spokój”, „olej to”. Przy pytaniach o rzeczy wrażliwe („Czy otworzyć drzwi?”) samo „nie” znaczy „nie rób tego” i kończy rozmowę. Odpowiedź czasownikiem jest oceniana w kontekście pytania: na „Czy włączyć…?” samo „włącz” to tak, a „wyłącz” to nie. Dla zamków i rolet kod używa ich własnych czasowników (w HA HassTurnOn dla zamka = zarygluj). Inna wypowiedź unieważnia pytanie.
  5. Wykonanie przez intent.async_handle (HA):
    • Encje-bliźniaki. Jedno urządzenie bywa widoczne w dwóch integracjach pod tą samą nazwą i w tym samym pokoju, np. HomePod z integracji Apple TV i z Music Assistanta („Łazienka HomePod” ×2). Intencja HA odrzuca taką nazwę (DUPLICATE_NAME) i nic nie robi, a nie da się w niej wskazać entity_id. Integracja nie pyta więc o „kilka pasujących urządzeń”, tylko wywołuje usługę na każdej z tych encji (media_player.volume_set, homeassistant.turn_on, light.turn_on …); pauza i następny utwór - tylko na tej, która gra. Nazwa podana przez model razem z pokojem („Łazienka HomePod”) jest porównywana bez słowa pokoju.
    • sprawdza, czy encja jest udostępniona Assistowi;
    • jako domyślny bierze pokój głośnika („włącz światło” bez pokoju);
    • w odpowiedzi wymienia, co zostało przełączone: „Wyłączono: …”.
  6. Akcje: skrypty i sceny. Model dostaje listę udostępnionych skryptów z ich opisami oraz scen z listą tego, co ustawiają (HA nie ma pola opisu sceny). Polecenie, które nie pasuje do żadnego urządzenia, może więc trafić w akcję robiącą to, o co proszono, nawet przy przekręconym słowie („zapasz kawę” → „Ekspres - zrób kawę”, „przewietrzmy mieszkanie” → scena wietrzenia). Skrypt jest uruchamiany wprost; jego pola z wyborem satelity Assist dostają głośnik, z którego padło polecenie.
    • Dlaczego opis, a nie zdania: automatyzacja ze zdaniami rozumie tylko wpisane słowa, a opis mówi modelowi, co skrypt robi, więc pasuje do dowolnego sformułowania. Kroki (włącz ekspres, poczekaj, zaparz) i tak są w skrypcie.
    • Jak pisać opis: zacznij od prostego zdania o efekcie („Parzy kawę: …”, „Zamyka wszystkie rolety zewnętrzne: …”) - model widzi pierwsze 160 znaków. Szczegóły techniczne dopisz dalej.
    • Co udostępniać: tylko akcje do uruchamiania głosem. Skryptów technicznych (powiadomienia, aktualizacje) i alarmu nie udostępniaj - model mógłby je wybrać przez pomyłkę. Akcje, przy których asystent ma zawsze pytać, dopisz do confirm_entities.
    • Automatyzacji się nie udostępnia: „włącz automatyzację” w HA znaczy „włącz jej działanie”, a nie „uruchom ją teraz”. Czynność, którą chcesz wywoływać głosem, przenieś do skryptu.
  7. Bełkot z rozpoznawania mowy. Whisper czasem przekręca całe zdanie („Głup nastrój w pisarowie” zamiast „Włącz nastrój w salonie”). Temat z unclear: true łapie zdania, w których słowa do siebie nie pasują. Model odgaduje, co powiedziano - zna nazwy pomieszczeń i akcji - a asystent pyta: „Nie jestem pewien, czy dobrze usłyszałem. Czy chodziło o: włącz nastrój w salonie?”. Na „tak” poprawione zdanie przechodzi całą zwykłą drogę (zdania HA, automatyzacje, router); na „nie” asystent prosi o powtórzenie, „anuluj” kończy. Gdy nie da się zgadnąć - od razu „Nie zrozumiałem. Powtórz, proszę.” W opisie tematu warto napisać wprost, co tu nie należy: pytania i polecenia z jednym przekręconym słowem, gdy sens jest jasny, pytania o wiedzę, rozmowa. Nic nie jest wykonywane bez potwierdzenia. Asystent prosi o powtórzenie najwyżej raz z rzędu. Gdy drugi raz nie zrozumie (znów bełkot albo znów „nie” na jego propozycję), mówi „Przepraszam, nie rozumiem. Jeśli będziesz czegoś potrzebować, poproś mnie jeszcze raz.” i kończy rozmowę - mikrofon się zamyka. Każde zrozumiane polecenie albo pytanie zeruje licznik; liczy się każda rozmowa (głośnik) osobno.
  8. Zabezpieczenia.
    • Alarm tylko zdaniami. Polecenie ze słowami „alarm”, „czuwanie”, „ochrona”, „syrena”, „uzbrój”, „rozbrój” (także przekręconymi, np. „halaryn”), którego nie złapała automatyzacja ze zdaniami, nie idzie do modelu - asystent odpowiada podpowiedzią („Alarmem steruję tylko konkretnymi zdaniami, na przykład: wyłącz alarm…”). Test pokazał, że model potrafił wtedy wybrać scenę „Dzień dobry”.
    • Bez pomieszczenia - pokój głośnika. HA przy poleceniu bez obszaru i bez nazwy („wyłącz światło”) bierze wszystkie urządzenia tego rodzaju w domu. Integracja dopisuje więc pomieszczenie głośnika, z którego padło polecenie; bez znanego głośnika pyta „W którym pomieszczeniu?”. (22.10.2026 przekręcone „wyłącz alarm” zgasiło w ten sposób 64 światła.)
    • Duży zasięg. Więcej niż 8 urządzeń naraz bez „wszystkie”, „cały”, „każdy” w poleceniu - najpierw pytanie.
    • Zgadnięty rodzaj urządzenia. Gdy model wybrał np. światło, a w poleceniu nie ma ani nazwy urządzenia, ani słowa, które na światło wskazuje („wyłącz kalafiora w kuchni”) - pytanie „Czy dobrze rozumiem…?”.
    • Samo „tak” bez pytania („tak, zrób to”, „nie rób”) nie jest poleceniem i niczego nie uruchamia; „zrób jaśniej w salonie” jest poleceniem, choć zaczyna się od „zrób”.
    • Akcja zawsze jest uruchamiana. „Wyłącz telewizor i kino domowe” może trafić w skrypt „Telewizor - wyłącz” - a „wyłącz” na skrypcie w HA by go zatrzymał. Skrypt i scenę model może też pomylić - nazwy szuka się w obu.
    • Nazwa skryptu bez modelu. Wypowiedziana dokładnie nazwa skryptu albo sceny (min. 3 słowa, np. „Opuść rolety wewnętrzne w salonie”) uruchamia ją od razu, bez rozbioru przez model.
  9. „Puść …” idzie inaczej: wyszukanie (media_player.search_media, np. w Music Assistancie) i odtworzenie pierwszego wyniku. Odtwarzacz wybiera kod: z music_players (pierwszy dostępny), bez wpisu odtwarzacz z wyszukiwaniem w tym obszarze albo z nazwą pokoju w nazwie, a bez pokoju w poleceniu pokój satelity. Światło bez obsługi koloru lub barwy dostaje komunikat zamiast błędu.
Zakres. Router steruje wszystkim, co jest udostępnione Assistowi. Urządzenia, których nie chcesz przełączać bez pytania (serwery, lodówka), wpisz do confirm_entities, a tych, których nie chcesz przełączać głosem wcale, nie udostępniaj.

11. Dziennik nierozpoznanych

/config/assist/unrecognized_commands.csv (UTF-8, separator ;) otwiera się w Numbers i Excelu. Kolumny:

KolumnaTreść
timestamp, area, devicekiedy i na którym głośniku
kindno_intent_match (HA nic nie rozpoznało) albo no_valid_targets (rozpoznało polecenie dla nieistniejącego obszaru lub urządzenia, np. „…w szkole”)
textco usłyszał Whisper
responseco odpowiedział asystent
routedroga routera: temat, liczba encji i źródeł (details, fetch), „+ odpowiedź” / „+ streszczenie”, a przy sterowaniu intencja i parametry
speakerrozpoznany mówca (Assist Speaker ID), puste = nieznany

Przeglądaj go co jakiś czas. Pytania z pustym route albo złą odpowiedzią pokazują, który temat poprawić. Częste zdania warto dopisać jako zwykłe zdania HA, bo te działają natychmiast.

12. Test i wybór modeli

W katalogu tools/ jest pięć narzędzi (Python, uruchamiane z dowolnego komputera z dostępem do Ollamy):

.venv/bin/python tools/model_benchmark.py --ollama http://localhost:11434 \
  --router-config /config/assist/router.yaml --cases model_test.yaml \
  --data model_test_data.yaml --router gemma4:e4b --answer gemma4:26b --pull --out wyniki.json
.venv/bin/python tools/model_report.py wyniki.json --label "test" \
  --html result.html --csv model_results.csv --answers-csv model_answers.csv
.venv/bin/python tools/compare_sources.py --ha http://homeassistant.local:8123 \
  --token-file ~/.ha_token --ollama http://localhost:11434 \
  --old router.yaml --new router.new.yaml --questions pytania.txt --out porownanie.json
.venv/bin/python tools/check_sentences.py --ha http://homeassistant.local:8123 \
  --token-file ~/.ha_token --config /config --per-template 5

Plik przypadków testowych (YAML) zawiera pytania z oczekiwanym tematem oraz dane z pytaniem i oczekiwanymi faktami. Format opisuje README.md. Trzymaj go poza repozytorium, bo zawiera dane Twojego domu.

13. Douczanie routera (opcjonalnie)

Ogólny mały model rozpoznaje tematy dobrze, ale można go douczyć na danych własnego domu. Służy do tego osobny zestaw skryptów, które w przyszłości trafią do osobnego repozytorium. Do nauki potrzebna jest karta graficzna (np. RTX 4090), a model po douczeniu działa w Ollamie jak każdy inny. Kroki:

  1. pobranie z HA obszarów, encji z aliasami i osób oraz plików ze zdaniami;
  2. zbiór danych:
    • zdania z Twoich szablonów hassil, z tematem przypisanym przez wzorcowe pytania z router.yaml;
    • wbudowane intencje HA z Twoimi nazwami;
    • pytania o wiedzę;
    • zakłócenia w stylu Whispera.
  3. LoRA (Unsloth) z dokładnie tym samym promptem, którego używa integracja;
  4. eksport do Ollamy;
  5. test na pytaniach, które nie były w nauce.

Po zmianie listy tematów douczony model trzeba douczyć ponownie.

Doświadczenie z pierwszej próby: Gemma 4 E2B douczona na samych zdaniach z szablonów rozpoznawała je w 99%, ale na swobodnych pytaniach z dziennika wypadła gorzej od zwykłej Gemmy 4 E4B (88/96 wobec 93/96). Nauczyła się stylu szablonów, a nie języka. Dlatego drugi przebieg łączy szablony z parafrazami, które pisze duży model (Gemma 4 26B): naturalne pytania z imionami, pokojami i błędami Whispera. Bazą jest E4B.

14. Prywatność i bezpieczeństwo