Programmatic way to make bulk rhasspy commands?

Apologies if i missed this somewhere in the documentation, I’ve read through a good chunk of it but i’m not sure what this feature would be called so i may have missed it if it exists.

Is there anyway to either bulk export intents from home assistant (or node red) or import anything into rhasspy help with the auto population of them?

As of right now i’m just manually connecting intents with node red and it seems like a very labor intensive bottle neck in getting rhasspy to really shine with it’s home assistant (or other home automation) integration.

One of the reasons i want to know if there’s anything already existing in this space to help connect rhasspy with your smart home devices in a more “programmatic” way is because i’m a javascript developer and if there isn’t anything already existing here, i could whip something up to help streamline the rhasspy integration with home assistant/node red so i appreciate any info i can find!

First: Welcome to the Rhasspy forum!

Don’t know about Home Assistand or NodeRed, but generally spoken you may use Rhasspy API to inject all kind of stuff like intents or slots. E.g. in the FHEM implementation, there’s some pieces of code to collect all kind of relevant data to inject specific intents or slots.
For the entire thing see starting at https://svn.fhem.de/trac/browser/trunk/fhem/contrib/RHASSPY/10_RHASSPY.pm#L2894 (written in Perl and highly linked to the FHEM code itself, I’m sorry, but you may get an idea how it works in general).
Own intents are called “shortcuts” and transfer starts in #L2928.

The sentences.ini then looks somehow like (german):

[de.fhem:SetOnOff]
rooms=([(im|in dem|auf dem|in der|auf der)] $de.fhem.Room{Room})
devSetOnOff=($de.fhem.Device-SetOnOff{Device})
onOff=((an|ein){Value:on}|aus{Value:off})
an=((an|ein){Value:on})
aus=(aus{Value:off})
den=(den|die|das)
cmdmulti=(schalte|schalt|mache|mach|stelle|stell)
cmddrive=(fahre|fahr)
cmdopenclose=(öffne{Value:on}|(schließe|schließ){Value:off})
openclose=((hoch|auf){Value:on} | (zu|runter){Value:off})

(<cmdmulti>|starte) [<den>] <devSetOnOff> [<rooms>] <onOff>
<cmddrive> [<den>] $de.fhem.Device-blind{Device} [<rooms>] <openclose>
<cmdopenclose>[<den>] $de.fhem.Device-blind{Device} [<rooms>]
<cmdmulti> [<den>] $de.fhem.Device-blind{Device} [<rooms>] <openclose>

oh wow i was not expecting perl lol! I’ll be honest, perl is outside of my skillset, do you know if there is any kind of HTTP REST api for rhasspy?

I haven’t been able to find anything since i posted my question so i’ve actually been working on developing a simple browser based react app to scrape entities from home assistant and then automatically create a node red flow in home assistant and output a formatted text file to copy and paste into rhasspy’s sentence.ini

My goal is to ultimately make it require the least amount of user intervention to set up new voice automations on rhasspy that do something on home assistant.

When i actually have it working i’ll be sure to post it back here because i imagine it’ll help a lot of community members that use rhasspy + home assistant!

The mentionned Perl code just collects a lot of information from the running FHEM configuration, so basically (in FHEM) you don’t have to do much more than to assign a keyword to any light/blind/thermostat/… to identify it’s type - sounds much alike what you plan to do with your browser-based script.
This info is just packed and sent to the HTTP interface using POST method (which paths should be visible from the code, /api/sentences and /api/slots?overwrite_all=$overwrite (true or false)). There’ docu available in Reference - Rhasspy.

Thank you! That’ll help me a lot being able to just POST over the sentences.ini with a new one!

Here’s some sample results from my test system - not “nice” and optimized, but it may be helpful:
POST to /api/sentences

{"intents/de.fhem.Shortcuts.ini":"[de.fhem:Shortcuts]\nton aus\ndu bist cool\nWitze\nton an\n"}

POST to /api/slots?overwrite_all=true

{"de.fhem.Aliases":["lichtaussenterrasse","lichtaussenterrasse_ch2","stimmungsleuchte","mülleimer","lampe1","sonos","szgisbert","blind1","blind2","rollo schlafzimmer","klima wohnbereich"],"de.fhem.AllKeywords":["global_test","multimedia","rollladen","rolladen","lampen","licht","räume->schlafzimmer","home","rhasspy","12_heizraum","httpmod","homekit","cuarto de estar","h harmony","wohnzimmer"," hhh kk","test 2","test3","wasauchimmer","lichtaussenterrasse_ch2","lichtaussenterrasse","rollo schlafzimmer","tanke irgendwo","klima wohnbereich","stimmungsleuchte","mqtt2_ebusd_sc","mülleimer","szgisbert","blind2","lampe1","blind1","sonos"],"de.fhem.Color":[""],"de.fhem.Device":["lichtaussenterrasse_ch2","lichtaussenterrasse","rollo schlafzimmer","tanke irgendwo","klima wohnbereich","stimmungsleuchte","mqtt2_ebusd_sc","mülleimer","szgisbert","blind2","lampe1","blind1","sonos"],"de.fhem.Device-GetNumeric":["klima wohnbereich","sonos","blind1","blind2","rollo schlafzimmer"],"de.fhem.Device-GetOnOff":["lichtaussenterrasse","lichtaussenterrasse_ch2","sonos","stimmungsleuchte","blind1","blind2","mülleimer","lampe1","rollo schlafzimmer"],"de.fhem.Device-GetState":["mqtt2_ebusd_sc","tanke irgendwo"],"de.fhem.Device-MediaControls":["tanke irgendwo","mülleimer","lampe1"],"de.fhem.Device-SetNumeric":["klima wohnbereich","szgisbert","sonos","stimmungsleuchte","blind1","blind2","mülleimer","lampe1","rollo schlafzimmer"],"de.fhem.Device-SetOnOff":["lichtaussenterrasse","lichtaussenterrasse_ch2","szgisbert","sonos","stimmungsleuchte","blind1","blind2","mülleimer","lampe1","rollo schlafzimmer"],"de.fhem.Device-blind":["szgisbert","blind1","blind2","rollo schlafzimmer"],"de.fhem.Device-contact":[""],"de.fhem.Device-light":["stimmungsleuchte","mülleimer","lampe1"],"de.fhem.Device-lock":[""],"de.fhem.Device-media":["sonos"],"de.fhem.Device-motion":[""],"de.fhem.Device-presence":[""],"de.fhem.Device-scene":["sonos","stimmungsleuchte"],"de.fhem.Device-switch":["lichtaussenterrasse","lichtaussenterrasse_ch2"],"de.fhem.Device-thermometer":[""],"de.fhem.Device-thermostat":["klima wohnbereich"],"de.fhem.Group":["global_test","multimedia","rollladen","rolladen","lampen","licht"],"de.fhem.Group-SetNumeric":["rollladen","multimedia","lampen","global_test","licht","rolladen"],"de.fhem.Group-SetOnOff":["rollladen","multimedia","lampen","global_test","licht","rolladen"],"de.fhem.Group-blind":["rollladen","rolladen"],"de.fhem.Group-contact":[""],"de.fhem.Group-light":["lampen","global_test","licht"],"de.fhem.Group-lock":[""],"de.fhem.Group-media":["multimedia"],"de.fhem.Group-motion":[""],"de.fhem.Group-presence":[""],"de.fhem.Group-switch":[""],"de.fhem.Group-thermometer":[""],"de.fhem.Group-thermostat":[""],"de.fhem.MainRooms":["cuarto de estar","rhasspy","h harmony","home"],"de.fhem.MediaChannels":["SkyOnline","orf drei","orf zwei","orf eins","YouTube","Netflix"],"de.fhem.NumericType":["desired-temp","temperature","brightness","setTarget","volume","dim","pct"],"de.fhem.Room":["räume->schlafzimmer","home","rhasspy","12_heizraum","httpmod","homekit","cuarto de estar","h harmony","wohnzimmer"," hhh kk","test 2","test3","wasauchimmer"],"de.fhem.Room-blind":["cuarto de estar","rhasspy","home","homekit","räume->schlafzimmer"],"de.fhem.Room-contact":[""],"de.fhem.Room-light":["rhasspy","wohnzimmer","h harmony"],"de.fhem.Room-lock":[""],"de.fhem.Room-media":["rhasspy","wohnzimmer"],"de.fhem.Room-motion":[""],"de.fhem.Room-presence":[""],"de.fhem.Room-switch":["cuarto de estar"],"de.fhem.Room-thermometer":[""],"de.fhem.Room-thermostat":["cuarto de estar"],"de.fhem.Scenes":["( scene1 ){Scene:scene1}","( scene2 ){Scene:scene2}","( scene3 ){Scene:scene3}","( scene4 ){Scene:scene4}","( Sonnenuntergang Savanne ){Scene:id=J88yqZyARhlQh4q}","( Nordlichter ){Scene:id=lk39fWLRdxjTqVl}","( Sonnenuntergang Savanne ){Scene:id=968VgE7b29JysiY}","( Hell ){Scene:id=Kd35TY8qIb6pTax}","( Lesen ){Scene:id=qk7qNpKVeugEugh}","( Konzentrieren ){Scene:id=kcz2eYl2goK91Hz}","( Energie tanken ){Scene:id=iJiMFXkE57qvCJ7}","( Frühlingsblüten ){Scene:id=fmsyN3Ve4fYGqhG}","( Gedimmt ){Scene:id=nEY-TZ2qVEx6WpP}","( Sonnenuntergang Savanne ){Scene:id=SOcyx8ZWLqI73MR}","( Lesen ){Scene:id=s-co9adxMQsAsEz}","( Entspannen ){Scene:id=gbca4wF9Vni8oSb}","( Hell ){Scene:id=jJAU3n8QlDZI4Zh}","( Energie tanken ){Scene:id=ykGAH5vm9qguUhm}","( Nachtlicht ){Scene:id=yJ0tWdBhE-emL3q}","( Frühlingsblüten ){Scene:id=wZdk5cpy35oldlw}","( Entspannen ){Scene:id=MkBqmt1iphIRayB}","( Konzentrieren ){Scene:id=WGjGSBvWPfW0JH0}","( Tropendämmerung ){Scene:id=yzMdTFOtJPAIVel}","( Energie tanken ){Scene:id=1N0Ow9IjFvwJWOb}","( Tropendämmerung ){Scene:id=1a6rDrpjg-i33u8}","( Tropendämmerung ){Scene:id=Li5XQV2Ckvcl9ac}","( Konzentrieren ){Scene:id=ig9Hb2jZ6iP9X0E}","( Gedimmt ){Scene:id=7r1VzbwX5H5VVqW}","( Nordlichter ){Scene:id=UloxktBtdsVJEUg}","( Lesen ){Scene:id=IYY-Qkhs6577mil}","( Nordlichter ){Scene:id=l9gNnheN-NwDIgY}","( Entspannen ){Scene:id=g9hPzEGZkAR2g5j}","( Nachtlicht ){Scene:id=0F5VwJFcHpymkm2}","( Nachtlicht ){Scene:id=Ke4VCTMCsSNaIli}","( Hell ){Scene:id=QszaD6316SsOGNX}","( Frühlingsblüten ){Scene:id=amg18-TICTPiayR}","( Gedimmt ){Scene:id=oZRhLrscoEgC7Xu}"]}

I generate most of my slots files with node-red directly and then call the api to retrain. I manually created the sentences.ini file through the gui to reference the different slots, so if I change anything in my systems (add devices for home automation, new artists/albums/playlists in my Sonos library, etc) they automatically are added to Rhasspy.

I then have separate function nodes to handle each intent to de-reference the slots and control things

Could you go into some more detail into… well i guess everything you discussed lol. It sounds like it’s pretty close to what i’m working on.

How do you generate slot files with node-red directly? Does your node-red setup poll for new devices and add them to a slot file automatically?

would you mind sharing your work :slight_smile: ??

It’s a really big topic and the scripts are too big to include here but I’ll do my best

Some background first as my system is probably a bit more complex than someone who started with HA and added node-red.

My home automation system is really node-red, I use Home Assistant as the frontend as it’s the best UI I’ve seen but I had already built all my rule/automation handling in node-red and am more than happy with it (when I don’t break it :grinning: ). All my devices and current state information is persisted in node-red and it communicates directly with zigbee2mqtt, zwavejs2mqtt, deconz, tasmota, roborock, enphase, daikin, sonos, etc to collect current states and also to send automation commands, it also handles the interface with Rhasspy, so HA is only in the picture as a big remote control that can talk directly to most of the devices as well.
And before you ask this is because I originally started with MiCasaVerde Vera, then moved to Openhab, then to node-red, all this before HA was ready for prime time even as a UI. node-red was the only one that would scale to the size of my system and still run reasonably. I have approx 300 zigbee devices, 70 zwave, 30 tasmota, … you get the picture.
So HA is not really in the picture for me but the rest could be manipulated to use it as a data source if you wanted.

I have a function node that I wrote which I am happy to share but it is 382 lines long so probably shouldn’t include in this message. It basically goes through all the devices and also my music library lists (playlist, artist, albums) and turns them into individual lists. The slots folder structure is this
.:
am_names down_state hours hours_pm hours_to_pm mins minutes_to on_off_state prepositions rhasspy sonos up_state
common ha hours_am hours_to infoType minutes_past on_off_command pm_names recur_every shopping sonostype

./common:
colour percent presence room whatsubject word

./ha:
arm cover door fan grouplight light power switch zone

./rhasspy:
days months

./shopping:
shopping

./sonos:
artist control favourite playlist speaker volume

and my sentences.ini file is
[Ignore]
Yes

[Presence]
is ($common/presence) {thing} [at] (home|present|here|in the house|nearby|close)
(who is | whose) [at] ((home|present|here|in the house|nearby|close):whohome) {thing}

[What]
question = (what | whats | what is | whats the | what is the)

($common/whatsubject) {info} is it [in [the] ($common/room) {room}] [please]
[please] tell me [] [is] [the] ($common/whatsubject) {info} [in [the] ($common/room) {room}] [is]
[the] ($common/whatsubject) {info} [in [the] ($common/room) {room}] [please]
($common/whatsubject) {info} [please]
($common/whatsubject) {info} in [the] ($common/room) {room} [please]
how ((hot | cold | warm | cool) {info:temperature}) is it [in [the] ($common/room) {room}] [please]
how ((humid) {info:humidity}) is it [in [the] ($common/room) {room}] [please]
($common/whatsubject) {info}

[GetState]
open_closed = ((open | closed | up | down) {state})
fan_state = (on | off | low | medium | high) {state}
(is|are) [the] ($ha/door|$ha/cover) {thing} <open_closed>
(is|are) [the] ($ha/light) {thing} ($on_off_state) {state}
(is|are) [the] ($ha/fan) {thing} <fan_state>
(what is | whats) [the] (status | state) [of] ($ha/door | $ha/cover | $ha/light | $ha/fan) {thing}

[ChangeState]
up_down_state = ((($up_state):increase) | (($down_state):decrease))
cover_state = (((open | $up_state):increase) | ((close | closed | $down_state):decrease))

($on_off_command) ($on_off_state) {state} [all] [the] ($ha/zone | $ha/fan | $ha/light | $ha/grouplight | lights) {thing}
[($on_off_command)] [all] [the] ($ha/zone | $ha/fan | $ha/light | $ha/grouplight | lights) {thing} [to] ($on_off_state) {state}
lights {thing} [($on_off_state) {state}]
fans {thing} [($on_off_state) {state}]

[set] [all] [the] ($ha/cover | $ha/light | $ha/grouplight | lights | shutters | skylights | curtains) {thing} [to] $common/percent {state} [percent]
<up_down_state> {state} [all] [the] ($ha/cover | $ha/light | $ha/grouplight | lights | shutters | skylights | curtains) {thing} [by] [another] [$common/percent {by}] [percent]
[all] [the] ($ha/cover | $ha/light | $ha/grouplight | lights | shutters | skylights | curtains) {thing} <up_down_state> {state} [by] [another] [$common/percent {by}] [percent]
<up_down_state> {state} [all] [the] ($ha/fan | fans) {thing}
[all] [the] ($ha/fan | fans) {thing} <up_down_state> {state}

[set] [all] [the] [temperature] [(in|for)] ($ha/zone | zones) {thing} [temperature] [to] $common/percent {state} [degrees]
<up_down_state> {state} [all] [the] [temperature] [(in|for)] ($ha/zone | zones) {thing} [temperature] [by] [another] [$common/percent {by}] [(degree | degrees)]
[all] [the] [temperature] [(in|for)] ($ha/zone | zones) {thing} <up_down_state> {state} [by] [another] [$common/percent {by}] [(degree | degrees)]
<up_down_state> {state} [all] [the] [temperature] [(in|for)] ($ha/zone | zones) {thing}
[all] [the] [temperature] [(in|for)] ($ha/zone | zones) {thing} [temperature] <up_down_state> {state}

<cover_state> {state} [the] ($ha/cover) {thing}
($ha/cover) {thing} <cover_state> {state}
<cover_state> {state} ((all [the] shutters):All_Shutters) {thing}
((all [the] shutters):All_Shutters) {thing} <cover_state> {state}

[(set|change|make)] [all] [the] ($ha/light | $ha/grouplight | lights) {thing} [to] [the] [colour] $common/colour {colour}
make [the] [colour of] [all] ($ha/light | $ha/grouplight | lights) {thing} [the] [colour] $common/colour {colour}
[(set|change)] [all] [the] ($ha/light | $ha/grouplight | lights) {thing} [colour] temperature {colour} [to] $common/percent {state} [percent]
<up_down_state> {state} [all] [the] ($ha/light | $ha/grouplight | lights) {thing} [colour] temperature {colour} [by] [another] $common/percent {by} [percent]
[all] [the] ($ha/light | $ha/grouplight | lights) {thing} [colour] temperature {colour} <up_down_state> {state} [by] [another] $common/percent {by} [percent]
[all] [the] ($ha/light | $ha/grouplight | lights) {thing} <up_down_state> {state} [colour] temperature {colour} [by] [another] $common/percent {by} [percent]

[turn] [the] [speed] <up_down_state> {state} [(on | for)] [the] ($ha/fan) {thing} [speed]
[turn] <up_down_state> {state} [the] [speed] [(on | for)] [the] ($ha/fan) {thing} [speed]
[turn] [the] [speed of] ($ha/fan) {thing} [speed] <up_down_state> {state}
[set] [the] ($ha/fan) {thing} [speed] [to] [speed] (((off | zero | 0):0) | ((low | one | 1):1) | ((medium | two | 2):2) | ((high | 3 | three):3)) {state}

(arm | disarm) {state} [the] ($ha/arm) {thing}

[AutoFlags]
automation_prefix = (turn|switch|make)
automation_command = (((enable|on):on)|((disable|off):off)) {state}
automation_type = ((home:automation|home automation):Home | (fans|fan):Fans | (Air Con|Air Conditioner|Air Conditioning):AirCon | (Cool|Cooling):Cool | (Heat|Heating):Heat | (Lights|Light|Lighting):Lights | (lock|Locks|Door Locks|Locking):Locks | (shutters|shutter):Shutters | (resets|reset):Resets) {thing}

[<automation_prefix>] <automation_command> [all] [(automatic|automated|automation|automations)] [(for|on)] <automation_type> [automation]
[<automation_prefix>] [all] [(automatic|automated|automation|automations)] [(for|on)] <automation_type> [automation] <automation_command>

[SonosLists]
sonos_command = ((i feel like | play):play | add) {command}

<sonos_command> [some] [(music | songs) by] ($sonos/artist) {artist} [(music | songs)] [(on | in)] ($sonos/speaker | here) {speaker} [$sonostype] [at] [$common/percent {volume}] [percent]
<sonos_command> [some] [(music | songs) by] ($sonos/artist) {artist} [(music | songs)] [at] [$common/percent {volume}] [percent]
<sonos_command> [playlist] ($sonos/playlist) {playlist} [playlist] [(on | in)] ($sonos/speaker | here) {speaker} [$sonostype] [at] [$common/percent {volume}] [percent]
<sonos_command> [playlist] ($sonos/playlist) {playlist} [playlist] [(on | in)] [$sonostype] [at] [$common/percent {volume}] [percent]
<sonos_command> ($sonos/favourite) {favourite} [(on | in)] ($sonos/speaker | here) {speaker} [$sonostype] [at] [$common/percent {volume}] [percent]
<sonos_command> ($sonos/favourite) {favourite} [(on | in)] [$sonostype] [at] [$common/percent {volume}] [percent]

[SonosControl]
($sonos/control) {control} [at] [$common/percent {volume}] [percent]
($sonos/control) {control} [(on | in)] ($sonos/speaker | here) {speaker} [$sonostype] [at] [$common/percent {volume}] [percent]
(stop | pause):pause {control}
play {control}

[SonosVolume]
(set|change) [volume] [(on | in)] ($sonos/speaker | here) {speaker} [$sonostype] [to] ($common/percent) {volume} [percent]
(set|change) [volume] [(on | in)] [$sonostype] [to] ($common/percent) {volume} [percent]
(set|change) [volume] [to] ($common/percent) {volume} [percent] [(on | in)] ($sonos/speaker | here) {speaker} [$sonostype]
(set|change) [volume] [to] ($common/percent) {volume} [percent] [(on | in)] [$sonostype]
($sonos/volume) [volume] to ($common/percent) {volume} [percent] [(on | in)] ($sonos/speaker | here) {speaker} [$sonostype]
($sonos/volume) [volume] to ($common/percent) {volume} [percent] [(on | in)] [$sonostype]
($sonos/volume) {volumechange} [volume] [by] ($common/percent) {volume} [percent] [(on | in)] ($sonos/speaker | here) {speaker} [$sonostype]
($sonos/volume) {volumechange} [volume] [by] ($common/percent) {volume} [percent] [(on | in)] [$sonostype]
($sonos/volume) {volumechange} [volume] [(on | in)] ($sonos/speaker | here) {speaker} [$sonostype] [by] [($common/percent) {volume}] [percent]
($sonos/volume) {volumechange} [volume] [(on | in)] [$sonostype] [by] [($common/percent) {volume}] [percent]

[SonosInfo]
(Who is | who are | whose) ((they|this|singing|the artist|artist|band|the band|group|the group|performing|playing):artist) {info} [(they|this|singing|the artist|the artist|performing|playing)]
(What is | Whats | What | Which) [is] [the] [this] ((track|song|this|playing):track) {info} [is this]
(What is |Whats | What | Which) [is] [the] [this] ((album|disc):album) {info} [is this]

[Shopping]
clear [(my|the)] shopping [list]
(list|whats in|what is in):list [(my|the)] shopping [list]
add ($shopping/shopping) [and] [($shopping/shopping)] to [(my|the)] shopping [list]
(remove|delete|cancel):delete ($shopping/shopping) [and] [($shopping/shopping)] from [(my|the)] shopping [list]

[ControlTimers]
alarm_type = (alarm|timer) {type}
timer_type = ((alarm|timer):timer) {type}
alarm_commands = (set | cancel | stop) {command}

<alarm_commands> [($prepositions)] <alarm_type> [[for] [every] ($recur_every) {recurrence}] [for] ($minutes_to) {minute} [minutes] ((to|before):to) ($hours_to) {hour}
<alarm_commands> [($prepositions)] <alarm_type> [[for] [every] ($recur_every) {recurrence}] [for] ($minutes_to) {minute} [minutes] ((to|before):to) ($hours_to) {hour} [in the] ($am_names)
<alarm_commands> [($prepositions)] <alarm_type> [[for] [every] ($recur_every) {recurrence}] [for] ($minutes_to) {minute} [minutes] ((to|before):to) ($hours_to_pm) {hour} [in the] ($pm_names)

<alarm_commands> [($prepositions)] <alarm_type> [[for] [every] ($recur_every) {recurrence}] [for] ($minutes_past) {minute} [minutes] ((past:after):past) ($hours) {hour}
<alarm_commands> [($prepositions)] <alarm_type> [[for] [every] ($recur_every) {recurrence}] [for] ($minutes_past) {minute} [minutes] ((past:after):past) ($hours) {hour} [in the] ($am_names)
<alarm_commands> [($prepositions)] <alarm_type> [[for] [every] ($recur_every) {recurrence}] [for] ($minutes_past) {minute} [minutes] ((past:after):past) ($hours_pm) {hour} [in the] ($pm_names)

<alarm_commands> [($prepositions)] <alarm_type> [[for] [every] ($recur_every) {recurrence}] [for] [oh] ($hours) {hour} [hundred] [hours] [($mins) {minute}]
<alarm_commands> [($prepositions)] <alarm_type> [[for] [every] ($recur_every) {recurrence}] [for] [oh] ($hours_pm) {hour} [hundred] [hours] [($mins) {minute}] [in the] ($pm_names)
<alarm_commands> [($prepositions)] <alarm_type> [[for] [every] ($recur_every) {recurrence}] [for] [oh] ($hours) {hour} [hundred] [hours] [($mins) {minute}] [in the] ($am_names)

*<alarm_commands> [($prepositions)] ($minutes_to) {minute} [minutes] ((to|before):to) ($hours_to) {hour} <alarm_type> [[for] [every] ($recur_every) {recurrence}] *
*<alarm_commands> [($prepositions)] ($minutes_to) {minute} [minutes] ((to|before):to) ($hours_to_pm) {hour} [in the] ($pm_names) <alarm_type> [[for] [every] ($recur_every) {recurrence}] *
*<alarm_commands> [($prepositions)] ($minutes_to) {minute} [minutes] ((to|before):to) ($hours_to) {hour} [in the] ($am_names) <alarm_type> [[for] [every] ($recur_every) {recurrence}] *

*<alarm_commands> [($prepositions)] ($minutes_past) {minute} [minutes] ((past|after):past) ($hours) {hour}<alarm_type> [[for] [every] ($recur_every) {recurrence}] *
*<alarm_commands> [($prepositions)] ($minutes_past) {minute} [minutes] ((past|after):past) ($hours) {hour} [in the] ($am_names) <alarm_type> [[for] [every] ($recur_every) {recurrence}] *
*<alarm_commands> [($prepositions)] ($minutes_past) {minute} [minutes] ((past|after):past) ($hours_pm) {hour} [in the] ($pm_names) <alarm_type> [[for] [every] ($recur_every) {recurrence}] *

*<alarm_commands> [($prepositions)] ($hours_pm) {hour} [hundred] [hours] [($mins) {minute}] [in the] ($pm_names) <alarm_type> [[for] [every] ($recur_every) {recurrence}] *

*<alarm_commands> [($prepositions)] [oh] ($hours) {hour} [hundred] [hours] [($mins) {minute}] <alarm_type> [[for] [every] ($recur_every) {recurrence}] *

<alarm_commands> [($prepositions)] <timer_type> {type} [for] [oh] ($hours) {hour} [hundred] [hours] [and] [($mins) {minute}]
<alarm_commands> [($prepositions)] <timer_type> [for] ($common/percent) {minute} minutes
<alarm_commands> [($prepositions)] <timer_type> [for] ($common/percent) {seconds} seconds
<alarm_commands> [($prepositions)] <timer_type> [for] ($common/percent) {hour} hours [and] ($common/percent) {minute} minutes

Each room has a designation like Room1, Room2, Garage1, … and also a name like Room7 is John’s Room. So I end up with something that looks like this as the slot file for rooms
(Room three|Kitchen):Room3
(BBQ|Barbeque):BBQ
(BackVeranda|Back Veranda):BackVeranda
(Garage two|Garage):Garage2
(Bathroom):Bathroom
(CommsRoom|Comms Room):CommsRoom
(Room seven|Johns Room):Room7

And for lights (just one room not all of them) it generate the most likely ways I would refer to the lights and send back the actual device reference regardless of what name I speak, that looks like this:-
( Room seven Fan Light|Johns Room Fan Light|Johns Fan Light ):Room7FanLight
( Room seven Main Left Light|Johns Room Main Left Light|Johns Main Left Light ):Room7MainLeftLight
( Room seven Main Mid Light|Johns Room Main Mid Light|Johns Main Mid Light ):Room7MainMidLight
( Room seven Main Right Light|Johns Room Main Right Light|Johns Main Right Light ):Room7MainRightLight
( Room seven Main Light|Johns Room Main Light|Johns Main Light ):Room7MainLight
( Room seven Spare Light|Johns Room Spare Light|Johns Spare Light ):Room7SpareLight
( Room seven Test Light|Johns Room Test Light|Johns Test Light ):Room7TestLight

and similar for covers, fans, door/window sensors, power consumption, switches, AC zones, etc

The function is called regularly and if there are any changes or additions, new files are created as appropriate and a call made to http://localhost:22101/api/train to re-train Rhasspy (the port is because I was trying to run multiple instances of Rhasspy on my base server so I could play with both trained and open transcription mode simultaneously, but that’s a different story)

btw. Here is the code I said was to big, it basically creates all the lists as arrays as it iterates through all my devices and at the end sends the lists as MQTT messages to an instance of node-red that sits on the Rhasspy server. When that receives the messages, if they are different to the previous one, it writes the files and initiates the re-train

var lights = [];
var covers = [];
var shutters = [];
var curtains = [];
var skylights = [];
var doors = [];
var power = [];
var switches = [];
var fans = [];
var favourites = [];
var speakers = [];
var rooms = [];
var zones = [];
var arm = [];
var words = [];
var voiceids = [“anyone”];
var percent = “”;
var artists = “”;
var info = [“time”,
“date”,
“temperature”,
“humidity”,
“weather”,
“(status of the washing|washing status|washing):washing”,
“(power|power consumption|power production):power”,
“sunrise”,
“sunset”];
var digits = [“zero”, “one”, “two”, “three”, “four”, “five”, “six”, “seven”, “eight”, “nine”, “ten”, “eleven”, “twelve”, “thirteen”, “fourteen”, “fifteen”, “sixteen”, “seventeen”, “eighteen”, “nineteen”];
var tens = ["", “”, “twenty”, “thirty”, “forty”, “fifty”, “sixty”, “seventy”, “eighty”, “ninety”];
var numbers = [];

for (i = 0; i <= 999; i++)
{
numbers[i] = “”;
if (i < 20)
{
numbers[i] = digits[i];
}
else if (i < 100)
{
if ((i % 10) === 0)
{
numbers[i] = tens[Math.floor(i / 10)];
}
else
{
numbers[i] = tens[Math.floor(i / 10)] + " " + digits[i % 10];
}
}
else if (i < 1000)
{
if ((i % 100) === 0)
{
numbers[i] = digits[Math.floor(i / 100)] + " hundred";
}
else if ((i % 100) < 20)
{
numbers[i] = digits[Math.floor(i / 100)] + " hundred and " + digits[i % 10];
}
else
{
numbers[i] = digits[Math.floor(i / 100)] + " hundred and " + tens[Math.floor((i) / 10)] + " " + digits[i % 10];
}
}
}
for (i = 0; i <= 100; i++)
{
percent = percent + “;;” + “( " + numbers[i] + " ):” + i;
}
//global.get(“artists”).forEach(artist =>
//{
// artists = artists + “;;(” + Object.keys(artist)[0].toLowerCase().replace(/-/g, " “).replace(/[^a-z0-9 ]/g,”") + “):” + Object.keys(artist)[0].replace(/[^A-Za-z0-9- ]/g,"");
//});
global.keys().forEach(thing =>
{
var roomname = “Unknown”;
var RoomName = [];
var item = global.get(thing);
var thing2 = “”;
var voicething = “”;

var tmpthing = thing.replace("_", " ").replace(/[^a-zA-Z0-9 ]/g, "").replace(/[A-Z]/g, function(x){return " " + x}).replace(/[0-9]/g, function(x){return " " + numbers[Number(x)]}).replace(" L ", " Left ").replace(" R ", " Right ").replace(/  */g," ").replace(" C P A P ", " CPAP ").trim();
tmpthing.split(" ").forEach(word =>
{
    if ((word.length > 3) && !words.includes(word))
    {
        words.push(word);
    }
});
Object.keys(item).forEach(key =>
{
    var tmpkey = key.replace("_", " ").replace(/[^a-zA-Z0-9 ]/g, "").replace(/[A-Z]/g, function(x){return " " + x}).replace(/[0-9]/g, function(x){return " " + numbers[Number(x)]}).replace(" L ", " Left ").replace(" R ", " Right ").trim().replace(/  */g," ").replace(" C P A P ", " CPAP ");
    tmpkey.split(" ").forEach(word =>
    {
        if ((word.length > 3) && !words.includes(word))
        {
            words.push(word);
        }
    });
});
flow.set("words", words);

if ((item.roomname !== undefined) && (item.roomname !== ""))
{
    var array;
    var room = global.get(item.roomname);
    if (room !== undefined)
    {
        var tmproomnames;
        var tmpzonenames;
        var tmpznames = [];
        var tmpnames = [];

        if ((room.roomname !== undefined) && (room.roomname !== ""))
        {
            roomname = room.roomname.replace(/[0-9]/, function(x){return " " + numbers[Number(x)]});
            tmpnames.push(roomname);
            if (room.roomname.startsWith("Room"))
            {
                tmpznames.push(roomname.replace("Room", "Zone"));
            }
        }
        if (room.Alias !== undefined)
        {
            room.Alias.toLowerCase().split("|").forEach(alias=>
            {
                if (! tmpnames.includes(alias))
                {
                    tmpnames.push(alias);
                }
                if (room.roomname.startsWith("Room"))
                {
                    if (! tmpznames.includes(alias))
                    {
                        tmpznames.push(alias);
                    }
                    if (! tmpznames.includes(alias.replace("Room", "Zone")))
                    {
                        tmpznames.push(alias.replace("Room", "Zone"));
                    }
                }
            });
        }
        if ((room.RoomName !== undefined) && (room.RoomName !== ""))
        {
            var rawRoomName = room.RoomName.replace(/[^A-Za-z0-9 ]/g, "");
            RoomName = [];
            RoomName.push(rawRoomName);
            if (!tmpnames.includes(roomname))
            {
                tmpnames.push(roomname);
            }
            if ((roomname !== rawRoomName) &&  (!tmpnames.includes(rawRoomName)))
            {
                tmpnames.push(rawRoomName);
            }
            if (room.roomname.startsWith("Room"))
            {
                if (! tmpznames.includes(roomname))
                {
                    tmpznames.push(roomname);
                }
                if ((roomname !== rawRoomName) && (! tmpznames.includes(roomname)))
                {
                    tmpznames.push(rawRoomName);
                }
            }
            
            ["Room", "Bedroom", "Study"].forEach(roomType =>
            {
                if (! RoomName.includes(rawRoomName.replace(" " + roomType, "")))
                {
                    RoomName.push(rawRoomName.replace(" " + roomType, ""));
                }
                if (room.roomname.startsWith("Room"))
                {
                    if (! tmpznames.includes(rawRoomName.replace(" " + roomType, "")))
                    {
                        tmpznames.push(rawRoomName.replace(" " + roomType, ""));
                    }
                    if (! tmpznames.includes(rawRoomName.replace(" " + roomType, " Zone")))
                    {
                        tmpznames.push(rawRoomName.replace(" " + roomType, " Zone"));
                    }
                }
            });
            
            tmproomnames = "(" + tmpnames.join("|") + "):" + item.roomname;
        }
        else
        {
            tmproomnames = "(" + roomname + "):" + item.roomname;
        }
        if ((tmproomnames !== undefined) && (tmproomnames !== "") && !rooms.includes(tmproomnames))
        {
            rooms.push(tmproomnames);
        }
        if (tmpznames.length > 0)
        {
            tmpzonenames = "(" + tmpznames.join("|") + "):" + item.roomname.replace(/^Room/,"Zone");
            if (!zones.includes(tmpzonenames))
            {
                zones.push(tmpzonenames);
            }
        }
    }
    var voicethings = [];
    voicethings.push(thing.replace(/[A-Z]/g, function(x){return " " + x}).replace(/[0-9]/g, function(x){return " " + numbers[Number(x)]}).replace(" L ", " Left ").replace(" R ", " Right ").trim().replace(/  */g," ").replace(" C P A P ", " CPAP "));
    for (i = 0; i < RoomName.length; i++)
    {
        thing2 = thing.replace(room.roomname, RoomName[i]);
        voicething = thing2.replace(/[A-Z]/g, function(x){return " " + x}).replace(/[0-9]/g, function(x){return " " + numbers[Number(x)]}).replace(" L ", " Left ").replace(" R ", " Right ").trim().replace(/  */g," ").replace(" C P A P ", " CPAP ");
        if (! voicethings.includes(voicething))
        {
            voicethings.push(voicething);
        }
    }
    if (item.Alias !== undefined)
    {
        item.Alias.toLowerCase().split("|").forEach(alias =>
        {
            if (! voicethings.includes(alias.replace(/[^a-z0-9 ]/g,"").trim()))
            {
                voicethings.push(alias.replace(/[^a-z0-9 ]/g,"").trim());
            }
        });
    }
    voicething = "( " + voicethings.join("|") + " ):" + thing;
    if (thing.endsWith("Light") || thing.endsWith("Lamp") || thing.endsWith("Bulb"))
    {
        lights.push(voicething);
    }
    else if (thing.endsWith("Shutter") || (thing.endsWith("Skylight") && (item.Control !== undefined)) || thing.endsWith("Curtain"))
    {
        covers.push(voicething);
    }
    else if (thing.endsWith("Door") || thing.endsWith("Window"))
    {
        doors.push(voicething);
    }
    else if (thing.endsWith("Power") || thing.endsWith("Powerpoint"))
    {
        power.push(voicething);
    }
    else if (thing.endsWith("Fan"))
    {
        fans.push(voicething);
    }
    else if (item.Switch !== undefined)
    {
        switches.push(voicething);
    }
    else
    {
        array = undefined;
    }
    if (item.Armed !== undefined)
    {
        arm.push(voicething);
    }
}
else if (thing.startsWith("RINCON_"))
{
    var aliases = [item.coordinator.roomName];
    global.keys().filter(room => global.get(room).Sonos === thing).forEach(room => 
    {
        var sonosroom = global.get(room + ".RoomName").replace(/[^A-Za-z0-9 ]/g, "");
        var alias;
        alias = room.replace(/[A-Z]/g, function(x){return " " + x}).replace(/[0-9]/g, function(x){return " " + numbers[Number(x)]})
        if (! aliases.includes(alias))
        {
            aliases.push(alias);
        }
        if (! aliases.includes(sonosroom))
        {
            aliases.push(sonosroom);
        }
        alias = alias.replace(" Bedroom", "").replace(" Study", "").replace(" Room", "");
        ["Room", "Study", "Bedroom"].forEach(roomtype => {
            alias = sonosroom.replace(" " + roomtype, "");
            if (! aliases.includes(alias))
            {
                aliases.push(alias);
            }
            if ((! alias.endsWith(" Room")) && (! aliases.includes(alias + " Room")))
            {
                aliases.push(alias + " Room");
            }
        })
    }
        );
    speakers.push("(" + aliases.join(" | ") + "):" + item.coordinator.roomName.replace(" ", "_"));
}
else if (thing === "info")
{
    var infoItem = "";
    Object.keys(item).forEach(key =>
    {
        var infoLineItems = [];
        var tmpitemname = "";
        tmpitemname = key.replace(/[A-Z]/g, function(x){return " " + x}).replace(/[0-9]/g, function(x){return " " + numbers[Number(x)]}).trim().replace(/  */g," ").replace(" C P A P ", " CPAP ")
        infoLineItems.push(tmpitemname);
        ["Provider", "Insurance", "Bank", "Policy"].forEach(utility =>
        {
            if (!infoLineItems.includes(tmpitemname.replace(utility, "").replace(/  */g," ").trim()))
            {
                infoLineItems.push(tmpitemname.replace(utility, "").replace(/  */g," ").trim());
            }
        });
        tmpitemname = "( " + infoLineItems.join("|") + " ):" + key;
        if (!info.includes(tmpitemname))
        {
            info.push(tmpitemname);
        }
    });
}
if (item.VoiceIds !== undefined)
{
    voiceids.push(item.VoiceIds);
}

});

global.get(“favorites”).forEach(favourite =>
{
var aliases = [];
var name = favourite.replace("&", " and “).replace(/ */g,” “);
var alias;
aliases.push(name.replace(/[^A-Za-z0-9 .]/g,”").trim().toLowerCase());

alias = name.replace(/\|.*/g,"").replace(/\(.*/g,"").trim().toLowerCase().replace(/[^A-Za-z0-9 \.]/g,"");
if (! aliases.includes(alias))
{
    aliases.push(alias);
}
alias = name.replace(/.*\(/g,"").replace(/\).*/g,"").trim().toLowerCase().replace(/[^A-Za-z0-9 \.]/g,"");
if (! aliases.includes(alias))
{
    aliases.push(alias);
}
alias = name.replace(/.*\(/g,"").replace(/\).*/g,"").trim().toLowerCase();
if (! aliases.includes(alias))
{
    aliases.push(alias);
}
alias = name.replace(/[0-9][0-9][0-9\.]*/g,"").replace(/\|.*/g,"").replace(/\(.*/g,"").trim().toLowerCase().replace(/[^A-Za-z0-9 \.]/g,"");
if (! aliases.includes(alias))
{
    aliases.push(alias);
}
favourites.push("(" + aliases.join(" | ") + "):" + favourite.trim().replace(/[^A-Za-z0-9]/g,"_"));

});
var grouplights = ["(dining lights|dining room lights):All_Room1Dining_Lights"];
rooms.forEach(room =>
{
grouplights.push(room.replace(/|/g, " lights|").replace(")", " lights)").replace(":", “:All_”) + “_Lights”);
});

node.send({topic: “slotlist/common/room”, payload: rooms.join(";;")});
node.send({topic: “slotlist/ha/zone”, payload: zones.join(";;")});
node.send({topic: “slotlist/ha/light”, payload: lights.join(";;")});
node.send({topic: “slotlist/ha/grouplight”, payload: grouplights.join(";;")});
node.send({topic: “slotlist/ha/cover”, payload: covers.join(";;")});
//node.send({topic: “slotlist/ha/curtain”, payload: curtains.join(";;")});
//node.send({topic: “slotlist/ha/skylight”, payload: skylights.join(";;")});
node.send({topic: “slotlist/ha/door”, payload: doors.join(";;")});
node.send({topic: “slotlist/ha/power”, payload: power.join(";;")});
node.send({topic: “slotlist/common/percent”, payload: percent});
node.send({topic: “slotlist/common/whatsubject”, payload: info.join(";;")});
node.send({topic: “slotlist/sonos/control”, payload: “(play|start|on):play;;(pause|stop|off):pause;;(mute:mute on):mute;;(on mute|un mute|mute off):unmute;;(back|previous|go to previous|change to previous):previous;;(next|forward|go to next|change to next):next;;(unshuffle|shuffle off|turn off shuffle|turn shuffle off):unshuffle;;(shuffle|shuffle on|turn on shuffle|turn shuffle on):shuffle;;(repeat|turn on repeat|repeat on|turn repeat on):repeat;;(unrepeat|on repeat|repeat off|turn repeat off):unrepeat”});
node.send({topic: “slotlist/sonos/volume”, payload: “(turn it up|turn up|up|increase|raise):increase;;(turn it down|turn down|down|decrease|lower):decrease”});
node.send({topic: “slotlist/ha/switch”, payload: switches.join(";;")});
node.send({topic: “slotlist/ha/fan”, payload: fans.join(";;")});
node.send({topic: “slotlist/ha/arm”, payload: arm.join(";;")});
node.send({topic: “slotlist/common/presence”, payload: voiceids.join(";;")});
node.send({topic: “slotlist/common/word”, payload: words.join(";;")});
node.send({topic: “slotlist/common/colour”, payload: global.get(“colours”).join(";;")});
node.send({topic: “slotlist/sonos/favourite”, payload: favourites.join(";;")});
node.send({topic: “slotlist/sonos/playlist”, payload: global.get(“playlists”).join(";;")});
node.send({topic: “slotlist/sonos/speaker”, payload: speakers.join(";;")});
//node.send({topic: “slotlist/sonosartist”, payload: artists});

return;

A lot of the logic is to sanitize the entries, ie. consistent case, remove irrelevant punctuation/spaces, remove duplicate terms, convert numbers to words, this all makes for a much easier (and less error prone) set of slots for Rhasspy to process and so it is just a bit faster at training and resolving the STT and intent recognition.

It is possible to read all entries from home assistant using the HA nodes in node-red, so it should also be possible to adapt the function’s code to use the HA entities rather than my node-red objects as the basis for the logic. You could also remove all the Sonos and music logic if you don’t have that.

oh wow, this is gold!!!

I can see you put a lot of passion and work into your home automation. It’s going to take me a while to digest everything you posted, but sincerely, thank you. This is a FANTASTIC resource!