Home Assistant does voice control through Assist. In Czech it comes down to two questions – which model recognises speech and synthesises the answer, and how natural sentences map to actions. Both are below – the model choice from my own testing and two sets of custom sentences, for energy consumption and the TV. Each part carries the date it was made.
Local vs. cloud models
Speech recognition (STT) ran locally through Whisper on an Intel NUC, the default model faster-whisper small-int8, Czech. Synthesis (TTS) through Piper, locally the cs_CZ-jirka-medium voice.
faster-whisper small-int8 local (NUC) ~5 s worseThe local model handled simple things, but its Czech comprehension was noticeably worse and the response slower. In the end cloud recognition through Nabu Casa stayed; the local Piper can still be used for synthesis.
Custom sentence architecture
Control splits into two layers:
- Sentences / Hassil intents in
custom_sentences/cs/– recognition of sentences and slots only. One file per topic (energy.yaml,tv.yaml). - Intent handling in
packages/voice_control.yaml–intent_scriptwith the actual HA actions.
configuration.yaml loads the packages:
homeassistant: packages: !include_dir_named packages
The intent name must be identical in both layers. Unknown intent AssistSetTvVolume means the sentence was recognised but no handler was found for it in intent_script.
Energy: power draw and consumption per period (23 October 2025)
The file custom_sentences/cs/energy.yaml recognises queries about the current power draw (optionally filtered) and about consumption over a period:
intents:
EnergyGetPowerByFilter:
slots:
device_filter:
optional: true
data:
- sentences:
- 'Jaký je aktuální příkon'
- 'Jaká je teď spotřeba'
- sentences:
- 'Jaký je aktuální příkon {device_filter}'
- 'Jaká je teď spotřeba {device_filter}'
EnergyGetEnergyPeriod:
slots:
period:
required: true
data:
- sentences:
- 'Kolik elektřiny {period}'
- 'Jaká je {period} spotřeba'
lists:
device_filter:
values:
- { in: 'shelly', out: 'shelly' }
- { in: 'zásuvka|zásuvky|zásuvek', out: 'socket' }
- { in: 'bojler', out: 'ariston' }
period:
values:
- { in: 'dnes|dnešní', out: 'day' }
- { in: 'tento týden|týdenní', out: 'week' }
- { in: 'tento měsíc|měsíční', out: 'month' }Handling EnergyGetPowerByFilter: with no spoken filter it uses the main Shelly 3EM sensor directly (device_class: power, state_class: measurement, unit W) and answers e.g. “Aktuální příkon celkem je 850 wattů.” With a filter it dynamically finds the power/active_power sensors matching {device_filter} and sums them.
EnergyGetEnergyPeriod maps {period} to sensor.energy_daily, sensor.energy_weekly and sensor.energy_monthly and returns consumption in kWh.
TV: volume and mute (30 October 2025)
The file custom_sentences/cs/tv.yaml handles two intents – setting the volume and toggling mute:
intents:
AssistSetTvVolume:
data:
- sentences:
- 'nastav hlasitost (televize|televizi|tv) (v|ve) {area} na {volume}'
- 'dej (televizi|tv) (v|ve) {area} hlasitost {volume}'
AssistToggleTvMute:
data:
- sentences:
- '{toggle} [zvuk] (televize|televizi|tv) (v|ve) {area}'
lists:
volume:
range:
from: 0
to: 100
toggle:
values:
- in: '(ztiš|ztlum|ztlumit|vypni zvuk|mute|umlč)'
out: 'mute'
- in: '(zapni zvuk|zruš ztlumení|unmute|zesil|zesílit)'
out: 'unmute'The {area} slot is an HA area, {volume} a number 0-100. The meaning is fixed by design – ztiš = mute, zesil = unmute – a mute toggle, not a gradual level change. Example command: “Nastav hlasitost televize v obýváku na 2.”
Handling AssistSetTvVolume: it converts the percentage to 0.0-1.0, finds the media_player.* in the area through area_id() and area_entities(), prefers an entity containing tv and, by supported_features, uses media_player.volume_set or the fallback volume_up/volume_down.
Mute is handled by the Philips TV (philips_js) integration, which does a real mute:
action: media_player.volume_mute data: is_volume_muted: true
That beats volume_set or volume_up/down – on mute the TV keeps its original volume and restores it on unmute. Volume fallbacks cannot guarantee that.
Things to watch out for
- Hassil
in:is not a YAML list. Writingin: ['ztiš', 'mute']fails withTypeError: expected string or bytes-like object, got 'list'. Write each variant as its own value, or as an alternationa|b|c. - Intent indentation.
data:must be nested under the intent; a wrong indent makes loading fail withAttributeError: 'NoneType' object has no attribute 'get'. - HA Jinja has no Python operators. Bitwise
feats & 4does not work; test a bit arithmetically,((feats // 4) % 2) == 1. Replace the globalabs(x)with thex | absfilter. - Tell the error layers apart. “Při zpracování došlo k neočekávané chybě” means the intent was recognised but its HA action failed – not a sentence error.
- Check entity availability. Philips
ConnectionFailure: All connection attempts failedis a problem of the integration talking to the TV, not of intent recognition. Before mute/unmute, callhomeassistant.update_entityand do not send the action when the player isunavailableorunknown.
_Energy came first in October 2025, the TV followed a week later; the text was last updated on 30 October 2025._