Rhasspy and Node Red Configuration

NOTE: First post and not a javascript programmer.

I wanted to create a flexible setup with Rhasspy and Node-Red in Homeassistant that wouldn’t require me updating Rhasspy AND my flow for each new item I added to my configuration. Currently, this is only set up for:

  • Lights
  • Fans
  • Covers
  • Switches

I only have lights currently, so I was unable to test this out on the other devices, but it should work. Essentially the sentences are set to allow me to simply add a new object within my object slot. This is needed to Rhasspy to interpret the object as a sentence. Once I do this, I don’t need to create a new Node-Red flow based on the new object, or even edit my existing one. Since the flow will gather all of the entities within Homeassistant, it can check to see if it exists based on the variables passed by Rhasspy.

Rhasspy Sentences

[TurnState]
(turn on | turn off){state} [the] [($room){room}] ($object){object}

[ChangeLightState]
(set | change) the [($room){room}] (light | lights){object} to (($colors){color_name} | (0…100){brightness_pct} percent)

[ChangeCoverState]
(open | close | set){state} the [($room){room}] ($covers){object} [to (0…100){position}]

[ChangeFanState]
(set | change) the [($room){room}] (fan){object} to (low|medium|high){speed}

Rhasspy Slots

colors

Blue
Red
....

covers

blinds
garage
window
shutters
gate
shutter
blind
curtains
awning
curtain
shade
door
shades
damper

room

bedroom
living room
loft
game room
master bedroom
kitchen
bathroom
guest bedroom
dining room
hallway

object

desk lamp
fan
shade
light
table 1
lights
shades

Node Red Flow

[{"id":"70d90eed.9fc7e8","type":"tab","label":"Rhasspy Intent","disabled":false,"info":""},{"id":"df68e344.5897","type":"api-call-service","z":"70d90eed.9fc7e8","name":"Execute","server":"9ca4f6d5.10c6d8","version":1,"debugenabled":false,"service_domain":"","service":"","entityId":"","data":"","dataType":"json","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":1140,"y":360,"wires":[["6001337d.9617cc"]]},{"id":"bee9de11.1d509","type":"function","z":"70d90eed.9fc7e8","name":"Initial Setup","func":"if (msg.slots.room) {\n    msg.slots.room = msg.slots.room.replace(\" \", \"_\");\n}\n\nif (msg.slots.object == \"lights\") {\n    msg.slots.object = \"light\";\n}\n\nmsg.slots.object = msg.slots.object.replace(\" \", \"_\");\n\nif (msg.slots.state) {\n    msg.slots.state = msg.slots.state.replace(' ', '_');\n} else {\n    msg.slots.state = \"\";\n}\n\nmsg.payload = {\n    \"data\": {\n        \"entity_id\": \"\",\n    },\n    \"domain\": \"\",\n    \"service\": \"\"\n}\n\nObject.entries(msg.slots).forEach(([key, value]) => {\n   if (key != 'room' && key != 'object' && key != 'state') {\n       msg.payload.data[key] = value;\n   }\n});\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":330,"y":360,"wires":[["558c9722.f9d858"]]},{"id":"6001337d.9617cc","type":"debug","z":"70d90eed.9fc7e8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1310,"y":360,"wires":[]},{"id":"558c9722.f9d858","type":"ha-get-entities","z":"70d90eed.9fc7e8","server":"9ca4f6d5.10c6d8","name":"Get Entities","rules":[],"output_type":"array","output_empty_results":true,"output_location_type":"msg","output_location":"entities","output_results_count":1,"x":530,"y":360,"wires":[["97fa5f32.9ca14"]]},{"id":"97fa5f32.9ca14","type":"function","z":"70d90eed.9fc7e8","name":"Find Object","func":"if (msg.entities) {\n    for (var val in msg.entities) {\n        currentEntity = msg.entities[val][\"entity_id\"];\n        \n        var cur = currentEntity.split('.');\n        domain = cur[0];\n        obj = cur[1];\n        \n        if (msg.slots.room) {\n            if (obj == msg.slots.room + '_' + msg.slots.object) {\n                //e.g. fan.living_room_fan\n                //e.g. cover.kitchen_blinds\n                msg.payload.data.entity_id = msg.slots.object + '.' + msg.slots.room + '_' + msg.slots.object;\n                msg.payload.domain = domain;\n                msg.payload.service = msg.slots.state;\n                break;\n            } else if (domain == msg.slots.object && obj == msg.slots.room) {\n                //e.g. light.living_room\n                msg.payload.data.entity_id = msg.slots.object + '.' + msg.slots.room;\n                msg.payload.domain = domain;\n                msg.payload.service = msg.slots.state;\n                break;\n            }\n        } else if (obj == msg.slots.object) {\n            //e.g. light.desk_lamp\n            //e.g. cover.blinds\n            //e.g. fan.ceiling_fan\n            msg.payload.data.entity_id = domain + '.' + obj;\n            msg.payload.domain = domain;\n            msg.payload.service = msg.slots.state;\n            break;\n        }\n    }\n    \n    //Some variations for lights, fans, and covers\n    if (msg.intent.name == \"ChangeLightState\") {\n        msg.payload.service = \"turn_on\";\n    } else if (msg.intent.name == \"ChangeCoverState\") {\n        if (msg.slots.state == \"open\" || msg.slots.state == \"close\") {\n            msg.payload.service = msg.slots.state + \"_cover\";\n        } else {\n            msg.payload.service = \"set_cover_position\";\n        }\n    } else if (msg.intent.name == \"ChangeFanState\") {\n        msg.payload.service = \"set_speed\";\n    }\n}\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":730,"y":360,"wires":[["5b1a1dc0.bf6e34"]]},{"id":"5b1a1dc0.bf6e34","type":"switch","z":"70d90eed.9fc7e8","name":"Check Entity ID","property":"payload.data.entity_id","propertyType":"msg","rules":[{"t":"nempty"}],"checkall":"true","repair":false,"outputs":1,"x":960,"y":360,"wires":[["df68e344.5897"]]},{"id":"13ddb6e0.cfcd39","type":"websocket in","z":"70d90eed.9fc7e8","name":"Rhasspy","server":"","client":"be111083.116b5","x":100,"y":340,"wires":[["bee9de11.1d509"]]},{"id":"9ca4f6d5.10c6d8","type":"server","name":"Home Assistant","legacy":false,"addon":true,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":true,"cacheJson":true},{"id":"be111083.116b5","type":"websocket-client","path":"ws://myRhasspyIP:myRhasspyPort/api/events/intent","tls":"","wholemsg":"true"}]

Initial Setup

Just set initial variables, and replace spaces with underscores, etc.
if (msg.slots.room) {
    msg.slots.room = msg.slots.room.replace(" ", "_");
}

if (msg.slots.object == "lights") {
    msg.slots.object = "light";
}

msg.slots.object = msg.slots.object.replace(" ", "_");

if (msg.slots.state) {
    msg.slots.state = msg.slots.state.replace(' ', '_');
} else {
    msg.slots.state = "";
}

msg.payload = {
    "data": {
        "entity_id": "",
    },
    "domain": "",
    "service": ""
}

//This is a trick to set variables like brightness_pct, color, and speed automatically
//set as slots in Rhasspy
Object.entries(msg.slots).forEach(([key, value]) => {
   if (key != 'room' && key != 'object' && key != 'state') {
       msg.payload.data[key] = value;
   }
});

return msg;

Get Entities

Just pull all entities and put in to msg.entities

Find Object

if (msg.entities) {
    for (var val in msg.entities) {
        currentEntity = msg.entities[val]["entity_id"];
        
        var cur = currentEntity.split('.');
        domain = cur[0];
        obj = cur[1];
        
        if (msg.slots.room) {
            if (obj == msg.slots.room + '_' + msg.slots.object) {
                //e.g. fan.living_room_fan
                //e.g. cover.kitchen_blinds
                msg.payload.data.entity_id = msg.slots.object + '.' + msg.slots.room + '_' + msg.slots.object;
                msg.payload.domain = domain;
                msg.payload.service = msg.slots.state;
                break;
            } else if (domain == msg.slots.object && obj == msg.slots.room) {
                //e.g. light.living_room
                msg.payload.data.entity_id = msg.slots.object + '.' + msg.slots.room;
                msg.payload.domain = domain;
                msg.payload.service = msg.slots.state;
                break;
            }
        } else if (obj == msg.slots.object) {
            //e.g. light.desk_lamp
            //e.g. cover.blinds
            //e.g. fan.ceiling_fan
            msg.payload.data.entity_id = domain + '.' + obj;
            msg.payload.domain = domain;
            msg.payload.service = msg.slots.state;
            break;
        }
    }
    
    //Some variations for lights, fans, and covers
    //This is only needed since services are different for covers and fans
    if (msg.intent.name == "ChangeLightState") {
        //We always need to set turn_on for lights when adjusting colors and brightness
        msg.payload.service = "turn_on";
    } else if (msg.intent.name == "ChangeCoverState") {
        if (msg.slots.state == "open" || msg.slots.state == "close") {
            msg.payload.service = msg.slots.state + "_cover";
        } else {
            msg.payload.service = "set_cover_position";
        }
    } else if (msg.intent.name == "ChangeFanState") {
        msg.payload.service = "set_speed";
    }
}

return msg;

Check Entity ID

Just check if entity_id is not empty

Execute

Execute current msg.payload, don't fill anything in here
1 Like

I’m curious what is the benefit of using this over Rhasspy’s built-in Home Assistant integration support?

I think I was trying to make at least the first sentence cover any type of object that you can turn on/off, and then determine what type of object that is. I haven’t tried the built-in integration much yet, but it might be easier. I was trying to go for something that could easily scale if I added 10 more lights, so I wouldn’t actually have to change anything in either Rhsaspy or NodeRed if I did this.

Your screenshot of Node-Red flow is not visible. Can you export node-red flow? All you have to do selects the nodes then click on the hamburger menu on the top right and click on export. Make sure you pick “selected nodes” because you may not want to share the entire flow or everything.

Done! Let me know if it’s not formatted correctly or something doesn’t look right. I did look more in to the integrated and built-in intents, which do look very similar. I may have not noticed them since I have rhasspy on a different raspberry pi in another docker environment.

Thank you for sharing, and I still like your idea of using node-red to check intents.

Hi everyone, I’m new to rhasspy and node red, could you help me fill in the fields if I want to turn a lamp on and off.

Is this flow still working?

I’ve tried to impliment it but it seems that the ‘Initial Setup’ node is not creating the data. values in msg.payload.data.

When I check the output of this node the only data object is entity_id which is blank as it should be according to the code.

The issue seems to be here:

Object.entries(msg.slots).forEach(([key, value]) => {
   if (key != 'room' && key != 'object' && key != 'state') {
       msg.payload.data[key] = value;
   }
});

If I replace one of the '!= ’ to ‘==’ that data object is created and the proper value is assigned (payload.data.room: kitchen)
Unfortunately the statement is not valid with all keys set to ==

I think changing the && to || creates a valid statement that creates and assigns the values to the data.object