Assist Router

A Home Assistant voice assistant that understands questions in Polish even when they do not match any sentence template. Everything runs locally.

Polski · English · GitHub

Language: Assist Router v0.1.0 understands Polish - its keywords, built-in phrases and spoken replies are Polish. The architecture (topics in router.yaml, data from entities, speaker recognition) is language-neutral; support for other languages is planned.

Setting up the whole voice stack from scratch (satellites, Whisper, Piper, Ollama, speaker recognition)? Start with the step-by-step guide (PRZEWODNIK.md, Polish), which walks you through it step by step and refers back here for the details.

1. What it is

The built-in Home Assistant assistant recognizes sentences using templates: HA intents, your own sentences in custom_sentences and automations with a sentence trigger. It is fast and reliable, but as soon as you phrase something differently („Na którą Ola idzie jutro do szkoły?” (“What time does Ola go to school tomorrow?”) instead of „O której Ola zaczyna lekcje?” (“When do Ola's lessons start?”)), you hear “Sorry, I couldn't understand that”. Adding more and more sentence variants never ends.

Assist Router is a conversation agent layered on top of the built-in HA agent. Everything HA understands keeps working unchanged. When HA does not recognize a sentence:

2. How it works

Speech Whisper (STT) HA agent intents, custom_sentences, automations Recognized answer unchanged Speaker Piper (TTS) Router (small model) topic only: {"temat": "lekcje"} Topic data entities, attributes, services trimmed to person and day Control parse -> HA intent confirm, intent.async_handle Wikipedia knowledge questions Fallback agent (chit-chat) Answer model answers the question from the data only; summarizes the article not recognized

Why the router returns only the topic

Small models make mistakes when they have to pick the topic, the person and the day and call a tool all at once. That is why the work is split like this:

As a result, answers are not made up, and a small model is enough for most tasks. The same split applies to home control: the model picks the intent and parameters from lists, the code matches the device, and HA executes the action.

Knowledge questions (Wikipedia)

  1. Search term. The code strips the beginning of the question („kim był” (“who was”), „co to jest” (“what is”), „kto jest obecnie” (“who is currently”), „kto napisał / wynalazł / wygrał” (“who wrote / invented / won”), „gdzie leży” (“where is”)…), so only the term itself is sent to the search: „Kto napisał Pana Tadeusza?” (“Who wrote Pan Tadeusz?”) → „Pana Tadeusza”.
  2. Typos. When the search returns nothing („…w piłce norznej”, a misspelling of “…in football”), the integration takes Wikipedia's spelling suggestion („…w piłce nożnej”) and searches again.
  3. Matching article. The article title must contain a word from the search term - the search engine always finds something, even for gibberish, and we do not read such an article.
  4. Several articles. The model gets the first article (up to 4000 characters) and the introductions of the next two matching ones (1000 each). For „Kto jest obecnie mistrzem świata…” (“Who is the current world champion…”) the first result is the upcoming tournament, while the answer is in the article about the most recent one.
  5. Answer. The model answers the question from these texts in 3–4 sentences.

3. Requirements and models

Proven in daily use: the whole setup runs every day in the author's home on an NVIDIA Jetson AGX Orin 64 GB: Ollama (Gemma 4 26B as both router and answer model), faster-whisper and Assist Speaker ID. It works very well: the router answers in about 0.7 s and an answer from data (router plus answer model) takes about 2 s. 64 GB of unified memory holds the model alongside Whisper.

Example results from an NVIDIA Jetson AGX Orin 64 GB. The test covered 96 router questions, 34 questions about real HA data and 8 Wikipedia summaries:

RoleModelAccuracyTime
RouterGemma 4 26B (same model as for answers, loaded in memory once)153/153 (incl. actions and gibberish)0.7 s
Router (smaller)Gemma 4 E4B92/960.5 s
Answer modelGemma 4 26B (MoE, 4B active)34/34 data, 8/8 summaries1.2 s / 5.5 s
Parsing control commandsGemma 4 26B (control_model: answer)67/67 commands (incl. actions)2.7 s
Guessing the sentence behind gibberishGemma 4 26Bout of 10 samples: 6 correct, 3× “don't know” (asks to repeat), 1 bold guess1.6 s
Lightweight setupGemma 4 E4B in all roles29/34 data, 8/8 summaries, 26/32 commands0.8 s / 3.8 s / 1.7 s

Full comparison of more than a dozen models (Gemma 4, Bielik, PLLuM, Qwen 3.5, Ministral – in Polish only, because the test used Polish questions): accuracy and time in each role, conclusions, typical errors and fine-tuning results. You can repeat the test on your own hardware: section 12.

Model memory. Ollama loads a model with the context size given in the request. The integration uses a fixed context: 4096 for the router and 8192 for the answer model. If the fallback agent in the Ollama integration uses the same model as the answer model, set its context to 8192 as well. Otherwise Ollama will reload the model on every question.

4. Installation

  1. Copy custom_components/assist_router into the /config/custom_components/ directory. Alternatively, add the repository to HACS as a custom repository of type “Integration”.
  2. Restart Home Assistant.
  3. Settings → Devices & services → Add integration → Assist Router.
  4. Settings → Voice assistants: in every pipeline change the conversation agent to “Assist Router” (entity conversation.assist_router) and turn off “Prefer handling commands locally”. With that option on, HA recognizes built-in intents before Assist Router: „Ogłoszenie, …” (“Announcement, …”) would be spoken by text-to-speech, and „Kto to mówi?” (“Who is speaking?”) and recording a voice sample would not take priority. Assist Router passes every sentence to the built-in agent first anyway, so nothing is lost.
  5. Copy router.example.yaml from the repository to /config/assist/router.yaml and adapt the entities to your home (file description: section 8).
  6. In the integration options, enter the Ollama address and choose the models (section 5).
  7. Optionally: Speaker ID, so the assistant knows who is asking (section 7, “Who is asking”). Assist Router reads the speaker by itself; you only need to change speech-to-text in your pipelines.

5. Integration options

Settings → Devices & services → Assist Router → Configure opens a menu:

ItemWhat it does
SettingsTwo steps, described below.
Router file (router.yaml)Edit /config/assist/router.yaml directly in HA. The file is validated before saving (invalid YAML or a configuration error shows a message and saves nothing), and the previous version is kept as router.yaml.bak. The router picks up changes from the next question, without a restart.
Example commandsPreview of sentences that work in this home, and generating them (see below).
Unrecognized commandsTable of the last 30 log entries (newest first) and a “Clear log” option; the previous content is kept as unrecognized_commands.csv.bak.

Example commands for your home

The assist_router.generate_examples action, or the “Example commands” menu item, works like this:

  1. For each topic in router.yaml, the answer model composes sentences using real names: the topic's entities, devices and rooms (for control), and household members.
  2. Every sentence goes through the real router. It is kept only if it lands in its own topic.
  3. Control commands also go through parsing and command planning, without being executed.

Result:

Generation takes a few minutes, because every sentence is a separate router request. Action options: per_topic (sentences per topic, default 5) and topics (only selected topics). The gibberish topic (unclear) is skipped.

The assist_router.clear_log action clears the unrecognized log. The “Unrecognized commands” sensor shows the number of entries, and its last attribute holds the last 20, which is handy for a Markdown card on a dashboard.

Settings, step one:

OptionWhat it does
Log unrecognized commandsAppends every unrecognized sentence to /config/assist/unrecognized_commands.csv and fires the assist_router_unrecognized event.
Fallback agentConversation agent for questions the router provided no data for (topic “other”, error). Empty = the message „Powiedziałeś: …” (“You said: …”).
Fallback agent timeoutAfter this time you hear „Powiedziałeś: …”.
Ollama addressE.g. http://localhost:11434. Empty = router disabled.

Step two appears once you enter the Ollama address. The model list is fetched from the server:

OptionWhat it does
Router modelPicks the topic. A small, fast model is enough.
Answer modelOptional, but required for answers from data. It writes the answer from the topic data and summarizes Wikipedia. Without it, data questions go to the fallback agent, and for Wikipedia the beginning of the article is read out.
Router / model timeoutDefault 8 s and 15 s. The router limit also applies to the Wikipedia search.
Keep model in memoryMinutes since the last question. -1 = always in memory, fastest response. Both models are loaded when the integration starts.

6. The router.yaml file

/config/assist/router.yaml describes your home. The integration code is the same for all users. Changes to the file take effect immediately, without restarting HA. Without the file the router is disabled. Below is a skeleton with the most important fields; a full example home with more than a dozen topics is in section 8, and how the code collects and trims data is described in section 7.

# Who lives in the home - goes into the router and answer model prompts
household: "Domownicy: Tomek (tata), Ania (mama), dzieci: Ola, Kuba."  # "Household: Tomek (dad), Ania (mum), children: Ola, Kuba."

# People in questions: match = word beginnings (regex), to catch Polish inflection
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: voice identifier in Assist Speaker ID (defaults to the person key, here "ola", "kuba")
  # owns: [calendar.kalendarz_oli] - this person's entities; room: Pokój Oli - their room

sentences: 4              # number of sentences in a Wikipedia summary
wikipedia_language: pl
only_exposed: false       # true = topic entities only from those exposed to Assist
control_model: answer     # which model parses control commands: router or answer
confirm_entities:         # always ask for confirmation (entity_id patterns or names)
  - switch.kuchnia_lodowka_przelacznik
  - switch.*_enabled
music_players:            # "play …": room -> players, first available wins
  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  # school, timetable, extra classes, when lessons end
    examples: ["Na którą Ola idzie jutro do szkoły?"]  # "What time does Ola go to school tomorrow?"
    details: [sensor.plan_lekcji]    # entity attributes, trimmed to the person and day in the question
    days: dzis                       # no day in the question - today
    drop: [teacher, classroom]       # keys to skip

  kalendarz:
    description: plany, wydarzenia, spotkania, urodziny  # plans, events, meetings, birthdays
    examples: ["Co mamy w weekend?"]  # "What do we have at the weekend?"
    days: "14"                       # no day in the question - 14 days from today
    fetch:                           # service with a response
      - action: calendar.get_events
        entities: [calendar.rodzina, calendar.praca]
        data: {start_date_time: "{start}", end_date_time: "{end}"}
        merge: events                # one list from all calendars, in order
        title: Wydarzenia w kalendarzach  # "Calendar events"

  samochod:
    description: auto - paliwo, zasięg, przebieg, czy zamknięte, gdzie stoi  # car - fuel, range, mileage, locked?, where parked
    examples: ["Ile paliwa ma auto?", "Czy zamknąłem samochód?"]  # "How much fuel is in the car?", "Did I lock the car?"
    entities: [sensor.moje_auto_*, binary_sensor.moje_auto_*, lock.moje_auto_*]

  swiatla:
    description: które światła są włączone  # which lights are on
    entities: [light.*, "!light.ipad_*"]   # "!" = exclusion
    skip_states: ["off"]

  sterowanie:
    description: polecenia - włącz, wyłącz, ustaw, przyciemnij, rolety, cieplej, głośniej  # commands - turn on/off, set, dim, blinds, warmer, louder
    examples: ["Zrób jaśniej w salonie.", "Opuść rolety do połowy."]  # "Make it brighter in the living room.", "Lower the blinds halfway."
    control: true

  wiedza:
    description: pytanie o wiedzę ogólną - kim był, co to jest, historia, geografia  # general knowledge - who was, what is, history, geography
    examples: ["Kim był Mikołaj Kopernik?"]  # "Who was Nicolaus Copernicus?"
    search: wikipedia

  inne:
    description: wszystko inne - rozmowa, żart, polecenia  # everything else - chit-chat, jokes, requests
    examples: ["Opowiedz coś ciekawego."]  # "Tell me something interesting."

Topic fields

FieldMeaning
descriptionA short description for the router: what questions in this topic are about. Topic descriptions should not overlap.
examples2–4 example questions. They help most with phrasings on the border between two topics.
entitiesentity_id patterns (* = any string, |class1,class2 = only these sensor device classes, !pattern = exclusion). Their states go into the data in Polish: a window is „otwarte” (“open”), a lock is „zamknięty” (“locked”). unknown/unavailable are skipped; at most 80 entities, in pattern order (most important first).
detailsPatterns of entities whose attributes are sent to the model (e.g. school timetable, account balances, tram departures). Lists are trimmed to the person in the question (key child or person) and to the day (key date or datetime); noise (html, avatars, colors, false values) is skipped, and times are local and include the weekday.
fetchServices with a response: action, entities, data ({start} and {end} = the day range from the question), optionally merge (a list to combine across all entities), title and limit (maximum number of items, default 40). E.g. calendars (calendar.get_events) or the forecast (weather.get_forecasts).
daysThe day used when the question does not give one: dzis (today), jutro (tomorrow), tydzien (week) or a number of days from today ("14"). Without this field, data is not filtered by day.
dropAttribute keys skipped in this topic.
personaltrue: the topic is for the person asking, even without „mam” (“I have”) and „moje” (“my”) („Dzień dobry” (“Good morning”)). A parent (family.parents) gets their own data and the children's data, a child only their own. An unknown speaker means everyone's data. A name in the question always wins.
answer_hintAn extra instruction for the answer model in this topic, e.g. how to structure a daily summary and what to say to a parent versus a child.
verbatimtrue: HA's answer to the template sentence in command is read out word for word, without the answer model. Several sentences separated by ; give all the answers in turn. Useful when an automation has a fixed answer layout, e.g. the daily news: „Dzisiejsze wiadomości z Polski: Pierwsza wiadomość…” (“Today's news from Poland: First item…”).
answer_hintsInstructions that depend on who is asking. Keys, in order: person (e.g. ola), girl/boy (children), child/parent, unknown (speaker not recognized). If no key matches, answer_hint is used. A simple instruction for one person works more reliably than one long instruction with conditions.
answer_tokensA longer answer than the default 160 tokens, e.g. 360 for a daily summary.
command(legacy) An HA (Jinja) template with a template question for your HA, e.g. an automation sentence. Separate several questions with ;. It is better to use details and fetch instead - no separate Assist automations needed.
search: wikipediaKnowledge topic: the search term is taken from the question and the article is summarized.
control: trueControl commands topic (section 10).
unclear: trueSpeech recognition gibberish topic: the model guesses what was said and the assistant asks „Czy chodziło o…?” (“Did you mean…?”); without a guess it asks you to repeat (section 10).
no data and no searchThe question goes to the fallback agent.

Variables in the command template (legacy field)

VariableValue
osobathe first name in the question (the form from people.name) or none
osobyall names in the question, in order: „Ola i Kuba” (“Ola and Kuba”) → ['Ola', 'Kuba']
ktothe key of the first person from people (e.g. ola)
kiedydzis, jutro, pojutrze, weekend, tydzien, a weekday (poniedzialek … niedziela, Monday … Sunday) or none
tekstthe full question, e.g. to extract a tram line number

Answer model prompts

The default prompts are built into the integration. You can override them in router.yaml with the fields answer_prompt (answer from data) and summary_prompt (summary). In the text you can use the fields {household}, {now} (date and time in Polish) and {sentences}.

7. Where the router gets its data

The router needs no special automations or Assist sentences. It takes data directly from Home Assistant, and in router.yaml you only describe which entities and services belong to a topic. The code does the rest: it picks out what was asked about, removes noise and gives the answer model a concise list of facts.

Question “What time does Ola go to school tomorrow?” Router (model) topic only: szkola Code person: ola · day: jutro Topic in router.yaml szkola: details, fetch, entities, days, drop entities entity states in Polish “Living room window: open (12 min)” details entity attributes timetable, balance, departures, address fetch services that return data calendar.get_events, weather.get_forecasts Trimming (code, no model) person: only items whose child/person matches “Ola” · day: only date/datetime from tomorrow no noise (html, avatars, colors, false) · local time and weekday · length limits Data for the model (concise YAML) usually a few hundred to a few thousand characters Answer model “Ola goes in at 8:00 tomorrow, maths is first.”

Three data sources

Topic fieldWhat goes to the modelTypical examples
entitiesThe state of every matching entity, in Polish, with the most important attributes: light brightness, blind position, thermostat temperature, what is playing, how long a sensor has been open.windows and doors, lights, room sensors, car, people (person.*), motion, alarm, washing machine
detailsEntity attributes, i.e. data you cannot see in the state itself: lists, timetables, balances, addresses.school timetable from the e-gradebook, departures from a stop, phone address (*_geocoded_location), pocket-money accounts
fetchThe response of an HA service that returns data. The day range comes from the question.calendars (calendar.get_events), forecast (weather.get_forecasts)

You can combine all three in a single topic. The weather topic in the example takes the current reading from sensors (entities) and the forecast from a service (fetch).

How the code trims data

Day from the question

In the questionValueDay range
„dziś”, „dzisiaj”, „teraz” (“today”, “now”)dzistoday
„jutro”, „pojutrze” (“tomorrow”, “the day after tomorrow”)jutro, pojutrzethat single day
„w poniedziałek” … „w niedzielę” (“on Monday” … “on Sunday”)poniedzialek … niedzielathe nearest such day (today, if it is today)
„w weekend” (“at the weekend”)weekendthe nearest Saturday and Sunday
„w tym tygodniu” (“this week”)tydzientoday and the next 6 days
nothingthe topic's dayse.g. dzis or "30" (today and the next 29 days)

Step-by-step example: school timetable

Question: „Na którą Ola idzie jutro do szkoły?”. The router picks the topic szkola, and the code extracts the person ola and the day jutro. The topic has details: [sensor.dziennik_plan_lekcji]. This entity's attributes contain the whole week for both children, well over ten thousand characters:

{"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": […]}]}

After trimming, the model gets only Ola's tomorrow, without avatars, colors and 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

The model's answer: „Ola idzie jutro na 8:00, pierwsza jest matematyka. Przyroda o 8:55 jest odwołana.” (“Ola goes in at 8:00 tomorrow, maths is first. Science at 8:55 is cancelled.”) The model noticed the cancelled lesson on its own, because the true flag stayed in the data.

Example: several calendars at once

calendar.rodzina calendar.praca_tomka calendar.urodziny calendar.swieta_w_polsce calendar.get_events “on Friday” → start: Friday 00:00 end: Saturday 00:00 merge: events 08:00 (Friday) Family: parents' evening 11:45 (Friday) Work: client meeting 16:30 (Friday) Family: Franek's birthday one list sorted by time, with source

Without merge, each calendar would be a separate source and the model would have to put the events in order itself. With merge, the question „Co mamy w piątek?” (“What do we have on Friday?”) gets a ready-made plan for the day, and „Kiedy są urodziny babci?” (“When is grandma's birthday?”) searches 30 days (days: "30").

fetch safety. The integration only calls services that only return data (in HA: SupportsResponse.ONLY, e.g. calendar.get_events, weather.get_forecasts, todo.get_items). An entry with a service that does something (lock.unlock, a script) is skipped with a warning in the log. A question never switches anything.

What about automations with sentences?

They are not needed. If you already have them, they can stay: HA checks them before the router, so a sentence that exactly matches an automation gets an answer in a fraction of a second, without a model. Any other phrasing goes to the router and the model answers it from the data. The legacy command field (a template question for an automation) still works, but new topics are better described with entities, details and fetch.

Who is asking: recognized speaker (Assist Speaker ID)

With the Speaker ID integration (speech-to-text that also recognizes the voice), the router knows who asked the question. You only need to set its STT („Mowa na tekst z mówcą”, “Speech-to-text with speaker”) in your pipelines. The agent stays Polski Assist, which reads the speaker by itself from the same pipeline run.

A person's entities and room (owns, room):

Daily summary and “good night” for a person: topics with personal: true collect the speaker's data. „Dzień dobry” (“Good morning”), „Jak wygląda mój dzień?” (“What does my day look like?”), „Co mnie dziś czeka?” (“What's ahead of me today?”) from a child give their lessons, tests, chores and calendar. From a parent they give the parent's calendar and the weather, plus a short summary about the children. How to structure the answer is set by the topic's answer_hint. Example: the podsumowanie_dnia and dobranoc topics in router.example.yaml. Scenes with these names are only run when asked explicitly („Uruchom scenę Dzień dobry” (“Run the Good morning scene”)). A plain „Dzień dobry” is a greeting.

Announcements (intercom): a sentence starting with „ogłoszenie” (“announcement”), „komunikat” (“message”), „do wszystkich” (“to everyone”) or „intercom” („Ogłoszenie, dzieci proszę przyjść na obiad” (“Announcement, children please come for lunch”)) goes neither to HA nor to the models. Assist Speaker ID plays the speaker's recording, uncut and unchanged, on the other speakers, and the speaker that recorded it only says „Ogłoszenie wysłane.” (“Announcement sent.”). Additional players, e.g. HomePods, are set in router.yaml as announce_players: [media_player.…]. Without a recording (text typed by hand or no Assist Speaker ID) the sentence continues along the normal path: HA reads it out with text-to-speech. These words must be at the start of the sentence, so „Jakie są ogłoszenia w Librusie?” (“What are the announcements in Librus?”) is an ordinary question.

Checking recognition: „Kto to mówi?” (“Who is speaking?”), „Kim jestem?” (“Who am I?”), „Jak mnie rozpoznajesz?” (“How do you recognize me?”), „Co to za głos?” (“Whose voice is this?”). The assistant answers immediately, without HA or models, from the result of that same utterance, e.g. „Mówi Ola, podobieństwo 82 procent. Następna osoba: Kuba, 35 procent.” (“Ola is speaking, 82 percent similarity. Next person: Kuba, 35 percent.”) The answer may also be „nie rozpoznaję tego głosu” (“I don't recognize this voice”) or „nie jestem pewien” (“I'm not sure”), with the two most similar voices. A short question (under 1 s of speech) gets a request for a longer sentence, e.g. „Powiedz mi, kto teraz do ciebie mówi” (“Tell me who is talking to you now”).

8. Example router.yaml

The router.example.yaml file in the repository describes a made-up home: Tomek, Ania and two children, Ola and Kuba. Entities are named the way popular integrations usually name them: the HA mobile app (phone location), Frigate (cameras), Met.no (weather), calendars, a car, a robot vacuum, a washing machine and shopping lists. Just replace the entity names with your own (Developer tools → States).

TopicData sourceWhat it shows
kalendarzfetch: calendar.get_events, mergeseveral calendars in one list, day from the question, 30 days if no day given
szkoladetails or fetchtimetable trimmed to the child and day; two e-gradebook variants
pogodaentities + 2 × fetchsensor reading plus daily and hourly forecast (limit: 24)
czujnikientities with |classsensors by device_class, exposed only
okna_drzwientitieswindows, doors, garage and blinds with the “since when” time
osobyentities + detailshousehold members, Frigate cameras, motion, address from the phone; pattern order
samochodentitiesall car sensors - the model picks
swiatlaentities, skip_states, !only lights that are on, excluding the tablet screen
mediaentities, skip_statesonly players that are playing something
listyentities: [todo.*]list items (the integration calls todo.get_items itself)
komunikacjadetailsdepartures from stops, from attributes
niezrozumialeunclear: truespeech recognition gibberish: a „Czy chodziło o…?” (“Did you mean…?”) suggestion or a request to repeat
sterowanie, wiedza, inne-commands, Wikipedia, fallback agent
# Example router.yaml for Assist Router - copy to /config/assist/router.yaml
# and adapt the entities to your home (Developer tools → States).
#
# The example home: Tomek (dad), Ania (mum), children Ola and Kuba. Entities are named the way
# popular integrations usually name them: the HA mobile app (phone location),
# Frigate (cameras), Met.no (weather), Google/CalDAV calendars, a car, robot vacuum
# and washing machine integration. All entity names are examples.
#
# How the router uses this file:
#   1. The router model picks ONE topic from `topics` (based on description and examples).
#   2. The code extracts the people (`people`) and the day ("jutro", "w piątek") from the question.
#   3. The topic collects data: entity states (entities), entity attributes (details) and service
#      responses (fetch) - trimmed to the person and day in the question.
#   4. The answer model answers the question using only this data.
# Changes to the file take effect immediately, without restarting HA.

# --- General settings ----------------------------------------------------------------

# Who lives in the home - goes into the prompts (the model knows that "mama" is Ania and
# "dzieci" (the children) are Ola and Kuba).
household: >-
  Domownicy: Tomek (tata, najczęściej pyta), Ania (mama), dzieci: Ola (Aleksandra, 10 lat)
  i Kuba (Jakub, 7 lat).
# ^ "Household: Tomek (dad, asks most often), Ania (mum), children: Ola (Aleksandra, 10) and Kuba (Jakub, 7)."

# People in questions. `match` is word beginnings (regex), to catch Polish inflection: "Oli", "Olą",
# "Kubie", "taty". A person from the question trims data that has a `child` or `person` key
# (e.g. only Ola's timetable). A question without a name ("dzieci") - everyone's data.
# With speaker recognition (the Assist Speaker ID integration) a first-person question ("Co mam jutro?")
# is about whoever is speaking. Extra person fields:
#   speaker - their voice identifier in Assist Speaker ID (defaults to the person key, e.g. "ola"),
#   owns    - their entities (e.g. work calendar, phone): skipped when asking about another person,
#             entities owned by no one (family calendar) - always included,
#   room    - their room: "zgaś światło w moim pokoju" from this person = this area.
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

# Family terms relative to the speaker: "mój brat" (my brother) from Ola = Kuba, "moja córka"
# (my daughter) from Tomek = Ola, "moje dzieci" (my children) from Ania = Ola and Kuba.
# "mama"/"tata" are fixed names (match above).
family:
  parents: [tomek, ania]
  children: [ola, kuba]
  men: [tomek, kuba]

sentences: 4                # number of sentences in a Wikipedia summary
wikipedia_language: pl
only_exposed: false         # true = topic entities only from those exposed to Assist
control_model: answer       # parsing control commands: router or answer (larger model)

# Control: for these devices the assistant always asks "Czy wyłączyć…?" (Turn off…?) - also when
# they are part of a group command ("wyłącz wszystkie przełączniki" - turn off all switches).
confirm_entities:
  - switch.lodowka
  - switch.serwer_*
  - switch.*_kamera_enabled     # camera switches (Frigate)
  - Router Wi-Fi                # you can also give a device name

# "Puść …" (play …): room -> players in order (first available). Music Assistant
# players often have no room - hence this list. With no room in the command: the room
# of the speaker you are talking to.
music_players:
  Salon: [media_player.salon_glosnik, media_player.salon_telewizor]
  Sypialnia: [media_player.sypialnia_glosnik]
  Kuchnia: [media_player.kuchnia_glosnik]

# --- Topics ---------------------------------------------------------------------------
#
# Topic data sources (can be combined):
#   entities  - entity_id patterns; the state goes to the model in Polish ("otwarte", "w domu").
#               `*` = any string, `|class1,class2` = only these device_class values,
#               `!pattern` = exclusion. Order matters (limit of 80 entities).
#   details   - patterns of entities whose ATTRIBUTES go to the model (lists, timetables, balances).
#   fetch     - services that return data (calendar.get_events, weather.get_forecasts…).
#               `{start}` and `{end}` = the day range from the question.
#   days      - the day when the question gives none: dzis, jutro, tydzien or a number of days.
#   drop      - attribute keys to skip (noise the model does not need).
#   skip_states - states to skip (e.g. lights that are off - only lights that are on remain).

topics:
  # Summaries for the person asking (requires Assist Speaker ID; without it - for the whole family).
  # personal: with no name in the question, the speaker's data, and for a parent also the children's.
  podsumowanie_dnia:
    # good morning, daily summary, what my day looks like, what's ahead today, plan for the rest of the day
    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
    # "This is a spoken daily summary. Talk to the person from 'Pyta:', by name, in plain sentences.
    #  Weather first, then their plan (lessons, events, chores). For a parent add one or two
    #  sentences about the children's most important matters. Up to 6 sentences."
    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  # good night, going to bed, what's ahead tomorrow
    examples: ["Dobranoc", "Idę spać", "Co mnie jutro czeka?"]
    personal: true
    days: jutro
    # speaker-dependent instruction: person, girl/boy, child/parent, unknown
    # girl: one or two sentences on what awaits her tomorrow (no home, no weather), then a tender,
    #       fairy-tale good night by name, as to a princess
    # boy: one or two sentences on what awaits him tomorrow, then a cheerful good night by name
    #      (e.g. recharging a superhero's batteries)
    # parent: what awaits them tomorrow and the morning weather, what was left open or on at home,
    #         one sentence about the children's tomorrow. End with "Dobranoc". Up to 5 sentences.
    # unknown: briefly for the whole family, no names: what's tomorrow and what is open or on at home
    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.*]


  # Calendars: events from all calendars in one list, in order.
  # "Co mamy w piątek?" (What do we have on Friday?) -> Friday only;
  # "Kiedy są urodziny babci?" (When is grandma's birthday?) -> 30 days from today.
  kalendarz:
    # plans, events, meetings, birthdays, name days, holidays, what we have planned, dates
    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]           # meeting descriptions (video call links) just take up space
    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             # one list from all calendars, sorted by time
        title: Wydarzenia w kalendarzach (po kolei)  # "Calendar events (in order)"
        limit: 60

  # School from the e-gradebook. Two typical variants - pick the one that fits:
  #  a) the integration provides an entity with attributes (children -> days -> lessons): `details`;
  #  b) the integration provides a lesson calendar for each child: `fetch` as for the calendar.
  szkola:
    # school, timetable, when lessons start or end, when they get back from school, substitutions, cancelled lessons
    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]      # variant a)
    days: dzis                                  # no day in the question - today
    drop: [teacher_id, classroom_id]
    # fetch:                                    # variant b)
    #   - action: calendar.get_events
    #     entities: [calendar.lekcje_ola, calendar.lekcje_kuba]
    #     data: {start_date_time: "{start}", end_date_time: "{end}"}
    #     merge: events

  # Weather: current conditions from the balcony sensors, forecast from a service (daily and hourly).
  pogoda:
    # outdoor weather now and forecast, rain, wind, take an umbrella?, what to wear, outside temperature
    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  # "Forecast for the coming days"
      - action: weather.get_forecasts
        entities: [weather.dom]
        data: {type: hourly}
        title: Prognoza godzinowa  # "Hourly forecast"
        limit: 24                 # one day at most - more only slows the answer down

  # Room climate: sensors by type (device_class) and thermostats.
  czujniki:
    # temperature, humidity, CO2, indoor air quality, heating, thermostats, is it stuffy
    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            # only sensors exposed to Assist (no technical ones)
    entities:
      - sensor.*|temperature,humidity,carbon_dioxide,pm25,volatile_organic_compounds
      - climate.*

  # Windows, doors and blinds: opening sensors and blind position.
  okna_drzwi:
    # which windows or doors are open, is everything closed, front door, balcony, garage, blinds
    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.*

  # Who is home and where: household members (person), address from the phone (HA mobile app),
  # Frigate cameras (number of people), motion sensors. Order: most important first.
  osoby:
    # who is home, where someone is, is anyone in a room, where was motion, how many people the cameras see
    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: number of people in frame
      - binary_sensor.*|motion,occupancy,presence
    details: [sensor.telefon_*_geocoded_location] # phone address (attributes: street, city)
    drop: [Location, Country, ISO Country Code, Postal Code, Time Zone, Areas Of Interest,
           Administrative Area, Sub Administrative Area, Ocean, Inland Water]

  # Car: all car sensors - the model picks what was asked about.
  samochod:
    # car - fuel, range, mileage, consumption, service, locked?, windows, tyres, where parked
    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_*

  # Lights: only those that are on (skip_states), excluding the tablet screen and speaker LED rings.
  swiatla:
    # which lights are on, where a light is on, was a light left on, lamp brightness
    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"]

  # What's playing: only players that are doing something.
  media:
    # what's playing now, music, TV, speakers, what volume, is the TV on
    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"]

  # Robot vacuum, washing machine - state and time remaining.
  odkurzacz:
    description: odkurzacz, robot sprzątający, czy skończył, bateria, filtr, szczotki  # vacuum, cleaning robot, finished?, battery, filter, brushes
    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  # washer, dryer, is the laundry done, how long left
    examples: ["Czy pralka skończyła?", "Ile jeszcze będzie suszyć?"]
    entities: [sensor.pralka_*, sensor.suszarka_*]

  # Lists (todo): list contents, not just the number of items (todo.get_items).
  listy:
    description: lista zakupów, co trzeba kupić, przypomnienia, rzeczy do zrobienia  # shopping list, what to buy, reminders, to-dos
    examples: ["Co jest na liście zakupów?", "Czy mamy na liście mleko?"]
    entities: [todo.*]

  # Alarm and safety - read-only (leave arming the alarm to automations).
  alarm:
    # alarm, is the alarm armed, protection mode, flooding, smoke, gas, is water leaking anywhere
    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

  # Public transport: departures from stops (MPK/ZTM integrations keep a list of departures
  # in attributes). Without a day - everything in the data.
  komunikacja:
    description: tramwaj, autobus, odjazdy z przystanku, za ile jest tramwaj  # tram, bus, departures from a stop, when is the next tram
    examples: ["Za ile mam tramwaj 8?", "Kiedy jedzie autobus z Parkowej?"]
    details: [sensor.przystanek_*]
    drop: [html, original_departure]

  # State of other devices (sockets, switches).
  urzadzenia:
    description: czy urządzenie jest włączone - gniazdka, przełączniki, ładowarki, akwarium  # is a device on - sockets, switches, chargers, aquarium
    examples: ["Czy akwarium jest włączone?", "Czy ładowarka w garażu działa?"]
    only_exposed: true
    entities: [switch.*]

  # Commands HA did not recognize: parsed into an HA intent and executed
  # (with a confirmation question when in doubt). Details: readme, section "Home control".
  sterowanie:
    # commands - turn on, turn off, switch off, switch on, set, dim, light color, blinds, warmer, louder, play music, vacuum, add to list
    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

  # General knowledge: a summary of a Wikipedia article (3–4 sentences).
  wiedza:
    description: pytanie o wiedzę ogólną - kim był, co to jest, historia, geografia, nauka  # general knowledge - who was, what is, history, geography, science
    examples: ["Kim był Mikołaj Kopernik?", "Co to jest fotosynteza?"]
    search: wikipedia

  # Speech recognition gibberish: the words do not fit together. The assistant guesses what was said
  # ("Czy chodziło o: włącz nastrój w salonie?" - Did you mean: turn on the mood scene in the living
  # room?) or asks you to repeat. State explicitly in the description what does NOT belong here -
  # otherwise the router will also put strange but meaningful questions here.
  niezrozumiale:
    # a nonsensical sentence - the words do not fit together and neither a question nor a command can be
    # understood; looks garbled by speech recognition. NOT for questions and commands with one garbled word
    # when the meaning is clear, nor for knowledge questions, chit-chat and jokes
    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

  # Everything else - fallback agent (chit-chat, jokes).
  inne:
    description: wszystko inne - rozmowa, żart, prośby, pytania bez danych w domu  # everything else - chit-chat, jokes, requests, questions without home data
    examples: ["Opowiedz coś ciekawego.", "Jak się masz?"]

9. How to add a new topic (step by step)

Example: questions about the washing machine and dryer, e.g. „Ile jeszcze będzie prać?” (“How much longer will the wash take?”) or „Czy mogę wyjąć pranie?” (“Can I take the laundry out?”).

  1. Find the entities. Developer tools → States, search e.g. for pralka. Check whether the information you need is in the state (e.g. “time remaining: 25 min”) - then entities is enough - or in the attributes (lists, timetables) - then use details.
  2. If the data comes from a service (calendar, forecast), try it in Developer tools → Actions with “Return response” checked. The same action with the same data goes into fetch; use {start} and {end} instead of dates.
  3. Add the topic:
      pranie:
        description: pralka, suszarka, czy pranie się skończyło, ile jeszcze potrwa  # washer, dryer, is the laundry done, how much longer
        examples: ["Czy suszarka skończyła?", "Ile jeszcze będzie prać?"]  # "Is the dryer done?", "How much longer will the wash take?"
        entities: [sensor.pralka_*, sensor.suszarka_*]
  4. Save and ask freely. In the CSV file, in the route column, you will see e.g. pranie + 12 encji + odpowiedź (12 entities + answer), and for details and fetch szkola + 1 źródeł + odpowiedź (1 source + answer).
  5. If the router picks the wrong topic, improve the description and add the question from the log to examples.
  6. If the answer is wrong but the topic is right, look at what the model received: the data is too long (add drop, days, limit), the answer is missing from it (add an entity), or it contains similar, confusing entities (exclude them with !pattern). tools/compare_sources.py helps (section 12).
Good practices

10. Home control

A topic with control: true handles commands that HA did not recognize. hassil sentences and automations always take priority.

  1. Whisper corrections. Certain corrections are applied immediately („czystkę” → „wszystkie” (“all”)), while uncertain ones (e.g. „jaś” → „zgaś” (“turn off”)) trigger a confirmation question. The list is WHISPER_FIXES in control.py.
  2. Parsing. The model returns schema-constrained JSON. The intencja (intent) field is an HA intent: HassTurnOn/Off, HassLightSet, HassSetPosition, HassClimateSetTemperature, HassSetVolume/Relative, media control, HassMediaSearchAndPlay („puść …” (“play …”)), vacuum or HassListAddItem. The fields domena (domain), obszar (area) and pietro (floor) are chosen from your HA's lists. On top of that come nazwa (name, a fragment of the utterance), wszystkie (all), wartosc (value) and zmiana (change: more / less), and for lights kolor (color, e.g. red) and barwa (color temperature: warm 2700 K, neutral 4000 K, cool 6500 K), and for music szukaj (search) and rodzaj (type: track, album, artist, playlist, genre, podcast).
  3. Matching in code.
    • The name fragment is compared with the names and aliases of entities exposed to Assist. Word stems are used, so Polish inflection is not a problem.
    • When the fragment is weak, the device whose name words were all said in the command wins.
    • An area that was not in the utterance is ignored. The model can confuse „garderobie” (“in the walk-in closet”) with „Garaż” (“Garage”).
    • Relative changes („jaśniej” (“brighter”), „wyżej” (“higher”), „cieplej” (“warmer”)) are calculated from the current state: by 25% or by 1°.
  4. A confirmation question („Czy wyłączyć Oświetlenie garderoby?” (“Turn off the closet lighting?”); the conversation stays open for 30 s) is asked when:
    • the Whisper correction was uncertain;
    • the command contains no known verb;
    • several devices match (but not twin entities - see below);
    • it concerns a lock, a gate or a garage door;
    • the device is on the confirm_entities list (also in a group command, e.g. „wyłącz wszystkie przełączniki” (“turn off all switches”)).
    With several similarly matching devices („roletą wewnętrzną okno” (“inner window blind”) - small and large) the assistant asks, instead of swapping in another device whose words happened to be said in the command (the outer window blind). Only a device with more words from the command wins („lampę w garderobie” (“the lamp in the closet”) → Garderoba Lampa). The question comes in two forms. When uncertain („Czy dobrze rozumiem, że mam uruchomić Ekspres - zrób kawę?” (“Do I understand correctly that I should run Coffee machine - make coffee?”)), a plain „nie” (“no”) means “you misunderstood”: the assistant apologizes, asks you to repeat and keeps listening, while „anuluj” (“cancel”), „nie trzeba” (“no need”) or „koniec” (“stop”) end the conversation. For sensitive devices („Czy otworzyć drzwi wejściowe?” (“Open the front door?”)) „nie” simply means “do nothing”. Replies work the same way for every question from the assistant: yes – „tak” (yes), „dokładnie (tak)” (exactly), „o to (mi) chodzi” (that's what I mean), „działaj” (go ahead), „zrób” (do it), „wykonaj” (execute), „dawaj” (go on); repeat – „nie” (no), „o coś innego” (something else), „nie o to chodzi” (that's not it), „jeszcze raz” (once more), „(może) powtórzę” (let me repeat) – the assistant apologizes and keeps listening; stop – „anuluj” (cancel), „koniec” (stop), „nieważne” (never mind), „zostaw (to)” (leave it), „nic nie rób” (do nothing), „daj (sobie z tym) spokój” (forget it), „olej to” (skip it). For questions about sensitive things („Czy otworzyć drzwi?” (“Open the door?”)) a plain „nie” means “don't do it” and ends the conversation. A reply with a verb is judged in the context of the question: to „Czy włączyć…?” (“Turn on…?”) a plain „włącz” (“turn on”) means yes, and „wyłącz” (“turn off”) means no. For locks and blinds the code uses their own verbs (in HA, HassTurnOn for a lock = lock it). Any other utterance cancels the question.
  5. Execution via intent.async_handle (HA):
    • Twin entities. A single device is sometimes visible through two integrations under the same name and in the same room, e.g. a HomePod from the Apple TV integration and from Music Assistant („Łazienka HomePod” (“Bathroom HomePod”) ×2). The HA intent rejects such a name (DUPLICATE_NAME) and does nothing, and there is no way to specify an entity_id in it. So the integration does not ask about “several matching devices”; instead it calls the service on each of these entities (media_player.volume_set, homeassistant.turn_on, light.turn_on …); pause and next track - only on the one that is playing. A name given by the model together with the room („Łazienka HomePod”) is compared without the room word.
    • it checks whether the entity is exposed to Assist;
    • it uses the speaker's room as the default („włącz światło” (“turn on the light”) without a room);
    • the reply lists what was switched: „Wyłączono: …” (“Turned off: …”).
  6. Actions: scripts and scenes. The model gets a list of exposed scripts with their descriptions, and of scenes with a list of what they set (HA has no description field for scenes). So a command that does not match any device can still hit an action that does what was asked, even with a garbled word („zapasz kawę” → „Ekspres - zrób kawę” (“Coffee machine - make coffee”), „przewietrzmy mieszkanie” (“let's air the flat”) → the airing scene). The script is run directly; its fields that select an Assist satellite receive the speaker the command came from.
    • Why a description and not sentences: an automation with sentences only understands the words typed in, while a description tells the model what the script does, so it matches any phrasing. The steps (turn on the coffee machine, wait, brew) are in the script anyway.
    • How to write the description: start with a simple sentence about the effect („Parzy kawę: …” (“Brews coffee: …”), „Zamyka wszystkie rolety zewnętrzne: …” (“Closes all outer blinds: …”)) - the model sees the first 160 characters. Add technical details after that.
    • What to expose: only actions meant to be run by voice. Do not expose technical scripts (notifications, updates) or the alarm - the model could pick them by mistake. Add actions the assistant should always ask about to confirm_entities.
    • Automations are not exposed: in HA, „włącz automatyzację” (“turn on the automation”) means “enable it”, not “run it now”. Move an action you want to trigger by voice into a script.
  7. Speech recognition gibberish. Whisper sometimes garbles a whole sentence („Głup nastrój w pisarowie” instead of „Włącz nastrój w salonie” (“Turn on the mood scene in the living room”)). A topic with unclear: true catches sentences whose words do not fit together. The model guesses what was said - it knows the names of rooms and actions - and the assistant asks: „Nie jestem pewien, czy dobrze usłyszałem. Czy chodziło o: włącz nastrój w salonie?” (“I'm not sure I heard you right. Did you mean: turn on the mood scene in the living room?”). On „tak” (“yes”) the corrected sentence goes through the whole normal path (HA sentences, automations, router); on „nie” the assistant asks you to repeat, and „anuluj” ends the conversation. When no guess is possible, it says straight away „Nie zrozumiałem. Powtórz, proszę.” (“I didn't understand. Please repeat.”) In the topic description it is worth stating explicitly what does not belong here: questions and commands with one garbled word when the meaning is clear, knowledge questions, chit-chat. Nothing is executed without confirmation. The assistant asks you to repeat at most once in a row. If it fails to understand a second time (gibberish again, or „nie” to its suggestion again), it says „Przepraszam, nie rozumiem. Jeśli będziesz czegoś potrzebować, poproś mnie jeszcze raz.” (“Sorry, I don't understand. If you need anything, just ask me again.”) and ends the conversation - the microphone closes. Every command or question that is understood resets the counter; each conversation (speaker) is counted separately.
  8. Safeguards.
    • Alarm only via sentences. A command with the words „alarm”, „czuwanie” (“armed/standby”), „ochrona” (“protection”), „syrena” (“siren”), „uzbrój” (“arm”), „rozbrój” (“disarm”) (also garbled, e.g. „halaryn”) that was not caught by an automation with sentences does not go to the model - the assistant replies with a hint („Alarmem steruję tylko konkretnymi zdaniami, na przykład: wyłącz alarm…” (“I only control the alarm with specific sentences, for example: turn off the alarm…”)). Testing showed that the model could otherwise pick the “Good morning” scene.
    • No room - the speaker's room. For a command without an area and without a name („wyłącz światło” (“turn off the light”)), HA takes all devices of that kind in the home. So the integration adds the room of the speaker the command came from; if the speaker is unknown, it asks „W którym pomieszczeniu?” (“In which room?”). (On 22.10.2026 a garbled „wyłącz alarm” (“turn off the alarm”) turned off 64 lights this way.)
    • Large scope. More than 8 devices at once without „wszystkie” (“all”), „cały” (“whole”), „każdy” (“every”) in the command - a question first.
    • Guessed device type. When the model chose e.g. a light, but the command has neither a device name nor a word that points to a light („wyłącz kalafiora w kuchni” (“turn off the cauliflower in the kitchen”)) - the question „Czy dobrze rozumiem…?” (“Do I understand correctly…?”).
    • A plain “yes” with no question pending („tak, zrób to” (“yes, do it”), „nie rób” (“don't”)) is not a command and does not run anything; „zrób jaśniej w salonie” (“make it brighter in the living room”) is a command, even though it starts with „zrób” (“do/make”).
    • An action is always run. „Wyłącz telewizor i kino domowe” (“Turn off the TV and home cinema”) may hit the script „Telewizor - wyłącz” (“TV - turn off”) - and “turn off” on a script in HA would stop it. The model may also confuse a script and a scene - the name is looked up in both.
    • Script name without the model. Saying the exact name of a script or scene (at least 3 words, e.g. „Opuść rolety wewnętrzne w salonie” (“Lower the inner blinds in the living room”)) runs it immediately, without parsing by the model.
  9. „Puść …” (“Play …”) works differently: a search (media_player.search_media, e.g. in Music Assistant) and playback of the first result. The code chooses the player: from music_players (first available); without an entry, a player with search support in that area or with the room name in its name; and with no room in the command, the satellite's room. A light without color or color temperature support gets a message instead of an error.
Scope. The router controls everything that is exposed to Assist. Put devices you do not want switched without a question (servers, the fridge) in confirm_entities, and do not expose those you never want controlled by voice.

11. Log of unrecognized commands

/config/assist/unrecognized_commands.csv (UTF-8, separator ;) opens in Numbers and Excel. Columns:

ColumnContent
timestamp, area, devicewhen and on which speaker
kindno_intent_match (HA recognized nothing) or no_valid_targets (it recognized a command for a non-existent area or device, e.g. „…w szkole” (“…at school”))
textwhat Whisper heard
responsewhat the assistant answered
routethe router's path: topic, number of entities and sources (details, fetch), „+ odpowiedź” (“+ answer”) / „+ streszczenie” (“+ summary”), and for control the intent and parameters
speakerrecognized speaker (Assist Speaker ID), empty = unknown

Review it from time to time. Questions with an empty route or a wrong answer show which topic to improve. Frequent sentences are worth adding as regular HA sentences, because those work instantly.

12. Testing and choosing models

The tools/ directory contains five tools (Python, run from any computer with access to Ollama):

.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

The test case file (YAML) contains questions with the expected topic, and data with a question and the expected facts. The format is described in README.md. Keep it outside the repository, because it contains your home's data.

13. Fine-tuning the router (optional)

A general small model recognizes topics well, but it can be fine-tuned on your own home's data. This is done with a separate set of scripts that will move to a separate repository in the future. Training requires a graphics card (e.g. an RTX 4090), and after fine-tuning the model runs in Ollama like any other. Steps:

  1. fetching areas, entities with aliases, people and sentence files from HA;
  2. the dataset:
    • sentences from your hassil templates, with the topic assigned via the template questions in router.yaml;
    • built-in HA intents with your names;
    • knowledge questions;
    • Whisper-style noise.
  3. LoRA (Unsloth) with exactly the same prompt the integration uses;
  4. export to Ollama;
  5. a test on questions that were not in the training data.

After the topic list changes, the fine-tuned model has to be fine-tuned again.

Lessons from the first attempt: Gemma 4 E2B fine-tuned on template sentences alone recognized them 99% of the time, but on free-form questions from the log it did worse than plain Gemma 4 E4B (88/96 versus 93/96). It learned the style of the templates, not the language. That is why the second run combines templates with paraphrases written by a large model (Gemma 4 26B): natural questions with names, rooms and Whisper errors. The base is E4B.

14. Privacy and security