> ## Documentation Index
> Fetch the complete documentation index at: https://docs.openhome.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Controlling MQTT Devices

> Control MQTT smart devices from your OpenHome DevKit — register your devices, use them in your Abilities, and control them by voice.

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.

<Note>
  MQTT device control requires an OpenHome DevKit. It cannot run from web Agents.
</Note>

## 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:

* [MQTT.org](https://mqtt.org) — the official protocol site
* [HiveMQ MQTT Essentials](https://www.hivemq.com/mqtt-essentials/) — an introductory guide

## 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.

```python theme={"system"}
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.

<Note>
  A `None` return value is expected, not an error. Confirm an action succeeded by observing the device, not by inspecting the return value.
</Note>

<Note>
  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](#registering-devices).
</Note>

### Parameters

| Parameter | Type  | Description                                                                              |
| --------- | ----- | ---------------------------------------------------------------------------------------- |
| `topic`   | `str` | MQTT topic of the target device.                                                         |
| `action`  | `str` | The operation to perform: `turn_on`, `turn_off`, or `custom`.                            |
| `value`   | `str` | Payload for a `custom` command. Ignored for `turn_on` and `turn_off`.                    |
| `command` | `str` | The device-specific command for a `custom` action. Ignored for `turn_on` and `turn_off`. |

### Actions

| `action`   | Description                      | Parameters used             |
| ---------- | -------------------------------- | --------------------------- |
| `turn_on`  | Powers the device on.            | `topic`                     |
| `turn_off` | Powers the device off.           | `topic`                     |
| `custom`   | Sends 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:

```python theme={"system"}
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`:

```python theme={"system"}
await self.capability_worker.send_devkit_mqtt_action(
    topic="living_room_light", action="custom",
    command="<device command>", value="<payload>",
)
```

<Note>
  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](#registering-devices).
</Note>

## 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.

<p align="center">
  <img src="https://mintcdn.com/algoryc/aA5PuuMCTML8iZBS/openhome_icons/dashboard_images/mqtt_config.png?fit=max&auto=format&n=aA5PuuMCTML8iZBS&q=85&s=b2265519bd7d9709f1841096283e7683" alt="OpenHome DevKit MQTT section showing the DevKit IP, broker credentials, and the Update MQTT button" style={{ borderRadius: '12px' }} width="1497" height="206" data-path="openhome_icons/dashboard_images/mqtt_config.png" />
</p>

### 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 setting        | Value                                                |
| --------------------- | ---------------------------------------------------- |
| Host / Broker address | The **DevKit IP** shown in the MQTT section.         |
| Port                  | The port shown in the MQTT section (default `1883`). |
| Username              | The username shown in the MQTT section.              |
| Password              | The password shown in the MQTT section.              |

<Note>
  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.
</Note>

## 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:

| Field           | Description                                                                                                                                                                                                                                   |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Name**        | A friendly name for the device, such as *Living Room Light*.                                                                                                                                                                                  |
| **Topic**       | The device's MQTT topic. This is the value passed as `topic` to `send_devkit_mqtt_action`.                                                                                                                                                    |
| **Command**     | The 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. |
| **Description** | A 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:

```python theme={"system"}
[
    {
        "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`:

```python theme={"system"}
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:

| Field           | Value                                                                                                        |
| --------------- | ------------------------------------------------------------------------------------------------------------ |
| **Name**        | Bedroom Light                                                                                                |
| **Topic**       | `tasmota_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`      |
| **Description** | RGBCW 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"`.

```python theme={"system"}
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

<Steps>
  <Step title="Capture the request">
    `main.py` waits for the user's full request with `wait_for_complete_transcription()`.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>
</Steps>

## Best practices

<AccordionGroup>
  <Accordion title="Give each device a clear command list">
    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.
  </Accordion>

  <Accordion title="Use custom for anything beyond power">
    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.
  </Accordion>

  <Accordion title="Point every device at the DevKit broker">
    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.
  </Accordion>

  <Accordion title="Confirm by observation, not by return value">
    `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.
  </Accordion>

  <Accordion title="Handle the empty-device case">
    When `self.worker.mqtt_devices` is empty, guide the user to register devices in the MQTT section rather than failing silently.
  </Accordion>
</AccordionGroup>

## See also

* [Local Abilities](/building-abilities/local-ability) — run code on the DevKit for direct, low-level device control
* [Home Assistant](/devkit/home-assistant/get-started) — install Home Assistant on the DevKit and control devices by voice
* [How to Build an Ability](/building-abilities/how-to-build) — the fundamentals of building an Ability
* [SDK Reference](/api-sdk/sdk-reference) — the full CapabilityWorker method catalog
