Feb 8, 2022

I’m in the process of replacing my voice assistants with a more private, home-based solution that keeps everything inside the local network. I had originally decided on using Rhasspy in a server/satellite configuration to collect voice commands, and Home Assistant to manage turning things on and off. I changed to openHAB after trying and failing for hours to get Home Assistant’s Docker image to listen to and act upon Rhasspy intents, plus running into other annoyances like having no clear way inside the Docker version to do basic things like group lightbulbs in the same lamp post in a way that made sense to me.

image by EFF/Hugh D’Andrade, source/license

A key part of the project is keeping everything local-only, so I didn’t want any accounts on any cloud services, and I wasn’t open to using more than a Docker image for uniformity with my other services. Unfortunately, I found Home Assistant’s Docker image to be a cut-down version of the software that seemed to require constant off-documentation modifications to YAML files to try to achieve the same functionality as running on bare metal or in a VM with a cloud account. I didn’t trust that I’d remember half of the changes I made or why I made them a year from now. openHAB, on the other hand, seemed at home in Docker and didn’t appear to lock any functionality behind creating off-site accounts.

I was still pretty set on using Rhasspy, though, as I was finding it quite friendly and intuitive. Notably, Rhasspy has a Home Assistant setting for intent handling, but none for openHAB, so I knew I’d need to go off the books a bit.

Luckily Rhasspy will emit MQTT events for commands you define, and openHAB can listen for MQTT events just fine. So long as you can capture the data inside those events, there’s nothing stopping you from having openHAB run actions based on that data.

This is where it gets a little tricky, though. How do you catch an MQTT event, read the data inside it, and then act on it in openHAB?

What follows assumes you have working installations of Rhasspy and openHAB, plus some devices added.

Disclaimer

This is almost certainly not the best way to do this, and may just be a flat-out awful way to do it. My total experience with these systems is about one day. I’d love to hear from you if you have advice on more official/correct ways of setting up this integration.

Install MQTT Binding and JSScripting

To get started, go to Settings in openHAB and click Bindings under Add-ons. You’ll need to install the MQTT Binding add-on. Next, go back to Settings and click Automations under Add-ons. Install the JSScripting add-on. Both of these are official add-ons provided by openHAB and pop right into your installation with zero drama or trips back to the command line.

MQTT Binding Add-on page

Create an “MQTT Broker” Thing

Next, create a new Thing in openHAB under Settings > Things. Choose “MQTT Binding” as the binding, then “MQTT Broker” as the Thing type. Fill in the field “Broker Hostname/IP”, then click “Show advanced” and fill in “Broker Port” if you’re not running Rhasspy’s MQTT on 1883 or 8883. By default Rhasspy’s Docker image seems to run MQTT over 12183, so it’s likely you’ll need to fill in this setting.

MQTT Broker configuration screen

Next, you’ll need to configure channels for each of your Rhasspy intents. You’ll find these intents in your Sentences screen in Rhasspy.

I have two intents defined so far, one for RGB lights, and one for what I’m calling “binary devices,” which just take ON or OFF commands (and which I just realized I should have called “boolean devices,” but there’s no way I’m going back and redoing these screenshots now). Apologies for the complication here, but it seems my RGB lights don’t respond to ON and OFF, but instead work by setting their color temperature to turn on, and setting their RGB to 0,0,0 to turn off.

Switch to the channels tab of your new Thing to copy these over into openHAB. I’ve added a ChangeStateRGB and ChangeStateBinary channel on the MQTT Broker Thing to catch these intents:

Here’s the channel configuration–it looks basically the same in each. Note the “MQTT Topic” (e.g. hermes/intent/ChangeStateRGB). This is how we listen only to these events we want to hear and nothing else.

At this point, save the Thing. This is done and won’t be edited again unless you add new intents in Rhasspy.

Create an “MQTT Handler” Rule

Next, we need to do something when an event happens on the ChangeStateRGB or ChangeStateBinary channel of the new MQTT Broker Thing. These events will be picked up automatically whenever Rhasspy interprets commands, but right now openHAB is just ignoring them. To start paying attention, go to Settings and click “Rules” under Automations, then add a new rule.

In the When section, add all your channels from the MQTT Broker Thing. Any time MQTT Broker receives an update, we want to activate this rule.

Under the Then section, click Add Action and choose Run Script. Choose the scripting language “ECMAScript 262 Edition 11” (or higher if you’re reading this years from now). openHAB ships with 262 edition 5.1 included, but from what I’ve read you’re missing access to some variables in there and edition 11 is going to be a smoother experience.

At this point, we want to parse the data attached to the MQTT event, then do something with it. Here’s what an MQTT event coming from Rhasspy will look like:

{
  "input": "turn off the TeaRoomFloorLamp",
  "intent": {
    "intentName": "ChangeStateRGB",
    "confidenceScore": 1
  },
  "siteId": "default",
  "id": "5833d3ae-30f5-4790-9e01-c16787a353bd",
  "slots": [
    {
      "entity": "state",
      "value": {
        "kind": "Unknown",
        "value": "off"
      },
      "slotName": "state",
      "rawValue": "off",
      "confidence": 1,
      "range": {
        "start": 5,
        "end": 8,
        "rawStart": 5,
        "rawEnd": 8
      }
    },
    {
      "entity": "RGBLights",
      "value": {
        "kind": "Unknown",
        "value": "TeaRoomFloorLamp"
      },
      "slotName": "name",
      "rawValue": "tea room floor lamp",
      "confidence": 1,
      "range": {
        "start": 13,
        "end": 32,
        "rawStart": 13,
        "rawEnd": 32
      }
    }
  ],
  "sessionId": "5833d3ae-30f5-4790-9e01-c16787a353bd",
  "customData": null,
  "asrTokens": [
    [
      {
        "value": "turn",
        "confidence": 1,
        "rangeStart": 0,
        "rangeEnd": 4,
        "time": null
      },
      {
        "value": "off",
        "confidence": 1,
        "rangeStart": 5,
        "rangeEnd": 8,
        "time": null
      },
      {
        "value": "the",
        "confidence": 1,
        "rangeStart": 9,
        "rangeEnd": 12,
        "time": null
      },
      {
        "value": "TeaRoomFloorLamp",
        "confidence": 1,
        "rangeStart": 13,
        "rangeEnd": 32,
        "time": null
      }
    ]
  ],
  "asrConfidence": null,
  "rawInput": "turn off the tea room floor lamp",
  "wakewordId": null,
  "lang": null
}

This data is going to come through as a message with some junk in front of the JSON, but by coercing it to a string, running a regex match for the outer brackets, then JSON.parse()ing it, we get a clean object (todo: rewrite this in a sturdier, safer manner). At that point we can pull relevant information out like the intent, state, and device.

One thing to note when writing your script is to use var instead of let or const. No idea why, but running the rule back-to-back will cause collisions with variables that have already been defined with let or const, but it seems to run it clean each time with var.

Here’s my script (so far) that parses the MQTT message, then runs openHAB actions based on the intent, device, and state it pulled out of it:

/* Get the data from the MQTT event
 * {
 *   json: Event Object,
 *   intent: String (intent name),
 *   state: String ("ON" | "OFF"),
 *   device: String (device name)
 * }
 */
var data = {};
data.json = JSON.parse((event + "").match(/\{.*\}/)[0]);
data.intent = data.json.intent.intentName;
data.state = data.json.slots.find(e => e.slotName === "state").value.value.toUpperCase();
data.device = data.json.slots.find(e => e.slotName === "name").value.value;


console.log("MQTT rule triggered: " + data.intent + ", " + data.device + " " + data.state);

// Perform actions by MQTT intent
// object "items", function "items.getItem()", and function "<item>.sendCommand()" are provided by openHAB
if (data.intent === "ChangeStateRGB") {
  
  // handle ChangeStateRGB event
  if (data.state === "ON") items.getItem(data.device + "_ColorTemperature").sendCommand("35");
  if (data.state === "OFF") items.getItem(data.device + "_Color").sendCommand("0,0,0");
  
} else if (data.intent === "ChangeStateBinary") {
  
  // handle ChangeStateBinary event
  items.getItem(data.device).sendCommand(data.state);
  
}

If you need some visibility into your data here, you can tail -f /openhab/userdata/logs/openhab.log. Anything you console.log() here will show up in your terminal each time the rule is triggered. Just throw some test data at Rhasspy to repeatedly trigger it.

Next Steps

This is probably workable for most things I’m doing, although next I’ll need to figure out how to turn groups of items and areas on and off and pull data for dimming. Also of note is no feedback is going back out from here, so I won’t be getting responses when commands are executed yet. I’m sure this can all be tacked on now that I have working core functionality, though.

Let me know if this comes in handy for you, and what additional things you end up doing with it!

Fork me on GitHub