Skip to main content
OpenHome lets your Agent control MQTT smart devices — such as lights, plugs, sensors, and appliances — through your OpenHome DevKit. Once a device is connected to your DevKit’s MQTT broker, an Ability can control it with the send_devkit_mqtt_action method by targeting the device’s MQTT topic. You can also register your devices in the dashboard. Registered devices are exposed to an Ability through self.worker.mqtt_devices, letting it discover the available devices at runtime — useful for voice Abilities that adapt to whatever devices a user has set up.
MQTT device control requires an OpenHome DevKit. It cannot run from web Agents.

New to MQTT?

MQTT is a lightweight publish/subscribe messaging protocol used widely by IoT and smart-home devices: a device subscribes to a topic, and commands are sent by publishing to that topic. This page assumes familiarity with that basic model. For a deeper introduction to the protocol itself, see:

Sending Actions with send_devkit_mqtt_action

send_devkit_mqtt_action is a CapabilityWorker method, called from an Ability’s main.py. It instructs the DevKit to publish an MQTT command to a device on its broker.
async def send_devkit_mqtt_action(self, topic="", action="", value="", command="")
The method is fire-and-forget: it returns None immediately and does not wait for the device to respond.
A None return value is expected, not an error. Confirm an action succeeded by observing the device, not by inspecting the return value.
You don’t need to register a device to control it. As long as the device is connected to your DevKit’s broker, you can target it directly by its topic. Registering a device only makes it discoverable through self.worker.mqtt_devices — see Registering Devices.

Parameters

ParameterTypeDescription
topicstrMQTT topic of the target device.
actionstrThe operation to perform: turn_on, turn_off, or custom.
valuestrPayload for a custom command. Ignored for turn_on and turn_off.
commandstrThe device-specific command for a custom action. Ignored for turn_on and turn_off.

Actions

actionDescriptionParameters used
turn_onPowers the device on.topic
turn_offPowers the device off.topic
customSends a device-specific command.topic, command, value
turn_on and turn_off require only topic; the command and value arguments are ignored. Use custom for any other operation — such as brightness, color, temperature, or device modes — supplying the command and payload that the device expects.

Examples

Power a device on and off:
await self.capability_worker.send_devkit_mqtt_action(
    topic="living_room_light", action="turn_on"
)

await self.capability_worker.send_devkit_mqtt_action(
    topic="living_room_light", action="turn_off"
)
Send a device-specific command with custom:
await self.capability_worker.send_devkit_mqtt_action(
    topic="living_room_light", action="custom",
    command="<device command>", value="<payload>",
)
The command and value for a custom action are specific to the target device. Record the commands a device supports when you register it, so an Ability has a reliable reference for what it can send. See Registering Devices.

Default MQTT Configuration

The DevKit’s MQTT broker is preconfigured and ready to use. To view or update it, open the dashboard and go to OpenHome DevKit → MQTT. This section shows the DevKit IP along with the broker’s username, password, and port (the default port is 1883). To change the broker’s username and password, use the Update MQTT button in this section. The DevKit applies the new credentials to its broker.

OpenHome DevKit MQTT section showing the DevKit IP, broker credentials, and the Update MQTT button

Connecting a Device to the Broker

A physical MQTT device must connect to the DevKit’s broker before it can be controlled. In that device’s own MQTT connection settings, enter the values shown in the MQTT section:
Device settingValue
Host / Broker addressThe DevKit IP shown in the MQTT section.
PortThe port shown in the MQTT section (default 1883).
UsernameThe username shown in the MQTT section.
PasswordThe password shown in the MQTT section.
Every device must connect to the same broker — the one running on your DevKit. Use the DevKit IP from the MQTT section as the broker host on each device.

Registering Devices

Registering a device makes it known to the DevKit and available to an Ability. Add a device from the OpenHome DevKit → MQTT section with the following fields:
FieldDescription
NameA friendly name for the device, such as Living Room Light.
TopicThe device’s MQTT topic. This is the value passed as topic to send_devkit_mqtt_action.
CommandThe list of commands the device supports. When an Ability uses the LLM to decide what to send, this list is the LLM’s reference — the clearer and more complete it is, the better the LLM can pick the right command and value for a request.
DescriptionA short description of the device, such as its room or type. The LLM uses this to identify the device and tell it apart from others, so keep it specific.
Registered devices become available to an Ability at runtime through self.worker.mqtt_devices — a list the Ability reads to discover its devices and decide which one to control. The Command and Description you enter here travel with each device in that list, so writing them clearly directly improves how well an LLM-driven Ability picks the right device and command. The next section covers how an Ability reads and uses this list.

Accessing Registered Devices with self.worker.mqtt_devices

Within an Ability, the registered devices are available as self.worker.mqtt_devices — a list of dictionaries, one per device:
[
    {
        "name": "Bedroom Light",
        "topic": "tasmota_channel",
        "commands": "/HSBColor hue,sat,bri (0-360,0-100,0-100); /Dimmer 0-100; /CT 153-500 (warm→cool); /Color R,G,B 0-255",
        "description": "RGBCW smart bulb — full RGB color plus tunable warm-to-cool white, dimmable. Controlled over MQTT (Tasmota).",
    },
    # one entry per registered device
]
An Ability reads this list to determine which devices are available, selects the appropriate one, and controls it using its topic:
devices = self.worker.mqtt_devices or []

target = devices[0]
await self.capability_worker.send_devkit_mqtt_action(
    topic=target["topic"], action="turn_on"
)
A common pattern is to pass the entire mqtt_devices list to the LLM, allowing it to map a spoken request to the correct device — using each device’s commands and description to determine the topic, action, and, for a custom action, the command and value. The example below follows this pattern.

Example: A Smart Home Ability

This Ability reads the registered devices from self.worker.mqtt_devices, sends the request and the device list to the LLM, and publishes the action the LLM returns with send_devkit_mqtt_action. Because the LLM acts as the orchestrator, the same code handles any registered device — to support a new device, register it in the dashboard; no code changes are required. Suppose a device like this is registered in the dashboard:
FieldValue
NameBedroom Light
Topictasmota_channel
Command/HSBColor hue,sat,bri (0-360,0-100,0-100); /Dimmer 0-100; /CT 153-500 (warm→cool); /Color R,G,B 0-255
DescriptionRGBCW smart bulb — full RGB color plus tunable warm-to-cool white, dimmable. Controlled over MQTT (Tasmota).
The Ability reads this through self.worker.mqtt_devices and passes it to the LLM, which uses the device’s command list and description to map a request like “make the bedroom light blue” to topic="tasmota_channel", action="custom", command="/HSBColor", and value="240,100,100".
import json
import re

from src.agent.capability import MatchingCapability
from src.agent.capability_worker import CapabilityWorker
from src.main import AgentWorker


# The LLM is the orchestrator: it reads the request + the device list and returns
# one JSON action. Tune this prompt to change behaviour — no code edits needed.
ORCHESTRATOR_PROMPT = """You control smart-home devices over MQTT. Read the user's request, pick the single device they mean, and decide the command to send it.

Devices:
{devices}
Each line gives a device's name, MQTT topic, and description, and may also list MQTT "commands" the device supports.

How to choose the device:
- Match on the device name, its location (e.g. "bedroom", "living room"), or its description.
- If several devices fit or you can't tell which one, ask instead of guessing.

How to choose the action:
- Simple power on/off → "turn_on" / "turn_off". Leave "command" and "value" empty.
- Anything else (brightness, color, temperature, modes, etc.) → "custom" with an MQTT "command" and "value":
    - If the device lists "commands", use them as your main reference, but you may still infer a command yourself when the request needs one that isn't listed.
    - If it lists no "commands", infer a sensible command and value from the device's name, description, and how such devices normally work over MQTT (e.g. Tasmota: Dimmer 0-100, HSBColor h,s,b, CT 153-500).
- Pick exactly one device. Keep "reply" natural, spoken, and under 20 words.

Reply with ONLY this JSON object, nothing else:
{{"topic": "<device topic>", "action": "turn_on" | "turn_off" | "custom", "command": "<command, custom only>", "value": "<value, custom only>", "reply": "<short spoken confirmation>"}}

If the request matches no device, several devices fit, or you can't determine a command, reply instead with:
{{"ask": "<one short clarifying question>"}}

Examples (illustrative only — use the real devices listed above):
- "turn on the kitchen light" -> {{"topic": "kitchen_light", "action": "turn_on", "command": "", "value": "", "reply": "Turning on the kitchen light."}}
- "dim the bedroom lamp to 30 percent" -> {{"topic": "bedroom_lamp", "action": "custom", "command": "Dimmer", "value": "30", "reply": "Setting the bedroom lamp to 30 percent."}}
- "make the living room bulb blue" -> {{"topic": "living_room_bulb", "action": "custom", "command": "HSBColor", "value": "240,100,100", "reply": "Turning the living room bulb blue."}}
- "turn on the light" (when several lights exist) -> {{"ask": "Which light do you mean — kitchen, bedroom, or living room?"}}

User request: {request}"""


class SmartHomeAssistantCapability(MatchingCapability):
    worker: AgentWorker = None
    capability_worker: CapabilityWorker = None

    # Do not change following tag of register capability
    # {{register capability}}

    def call(self, worker: AgentWorker):
        self.worker = worker
        self.capability_worker = CapabilityWorker(self)
        self.worker.session_tasks.create(self.run())

    def format_devices(self, devices: list) -> str:
        entries = []
        for device in devices:
            entry = f"- {device['name']} (topic: {device['topic']})"
            if device["description"]:
                entry += f" — {device['description']}"
            if device["commands"]:
                entry += f" [commands: {device['commands']}]"
            entries.append(entry)
        return "\n".join(entries)

    def decide(self, request: str, devices: list) -> dict:
        prompt = ORCHESTRATOR_PROMPT.format(
            devices=self.format_devices(devices),
            request=request,
        )
        history = self.capability_worker.get_full_message_history()
        raw = self.capability_worker.text_to_text_response(prompt, history)

        match = re.search(r"\{.*\}", raw or "", re.DOTALL)
        if match:
            try:
                parsed = json.loads(match.group(0))
                if isinstance(parsed, dict):
                    return parsed
            except json.JSONDecodeError:
                pass
        return {}

    async def run(self):
        try:
            request = await self.capability_worker.wait_for_complete_transcription() or ""

            devices = self.worker.mqtt_devices
            if not devices:
                await self.capability_worker.speak(
                    "There are no devices added. Please add your devices in the OpenHome DevKit MQTT section."
                )
                return

            action = self.decide(request, devices)

            # One clarification round if the model needs more detail.
            if action.get("ask"):
                answer = await self.capability_worker.run_io_loop(action["ask"]) or ""
                action = self.decide(f"{request}. {answer}", devices)

            if not action.get("topic"):
                await self.capability_worker.speak(
                    action.get("ask") or "Sorry, I couldn't tell which device you meant, please try again."
                )
                return

            self.worker.editor_logging_handler.info(f"[SmartHome] {action}")
            await self.capability_worker.send_devkit_mqtt_action(
                topic=action["topic"],
                action=action.get("action", "custom"),
                value=action.get("value", ""),
                command=action.get("command", ""),
            )
            await self.capability_worker.speak(action.get("reply", "Done."))
        except Exception as e:
            self.worker.editor_logging_handler.error(f"[SmartHome] Error: {e!r}")
            await self.capability_worker.speak("Something went wrong with that request.")
        finally:
            self.capability_worker.resume_normal_flow()

Interaction Flow

1

Capture the request

main.py waits for the user’s full request with wait_for_complete_transcription().
2

Load registered devices

The Ability reads self.worker.mqtt_devices. If no devices are registered, it asks the user to add them in the OpenHome DevKit → MQTT section and exits.
3

Decide the action

format_devices() renders the device list into the prompt, and the LLM returns a single JSON action — a topic and action (plus command and value for custom) with a spoken reply, or an ask when it needs clarification.
4

Clarify if needed

If the LLM returns ask, the Ability poses the question with run_io_loop() and decides once more with the added detail.
5

Publish the action

The Ability calls send_devkit_mqtt_action() with the resolved topic, action, command, and value. The DevKit publishes the command to the device.
6

Confirm and exit

Because the call is fire-and-forget, the Ability speaks the LLM’s reply to confirm, then calls resume_normal_flow() so the Agent returns to its normal flow.

Best practices

The Command field you set when registering a device is the reference an Ability and the LLM rely on. List the commands the device actually supports so requests map to valid commands.
Only turn_on and turn_off ignore command and value. For brightness, color, temperature, or device modes, use action="custom" with the device-specific command and payload.
A device is only controllable once it connects to the DevKit’s broker. Configure each device with the DevKit IP, port, and credentials from the MQTT section.
send_devkit_mqtt_action is fire-and-forget and returns None. Confirm the result by the device’s behaviour, and speak a short confirmation to the user.
When self.worker.mqtt_devices is empty, guide the user to register devices in the MQTT section rather than failing silently.

See also