Tech Expert & Vibe Coder

With 14+ years of experience, I specialize in self-hosting, AI automation, and Vibe Coding – building applications using AI-powered tools like Google Antigravity, Dyad, and Cline. From homelabs to enterprise solutions.

Integrating Claude Opus 4.5 Function Calling with Home Assistant: Converting Natural Language Commands to MQTT Actions

Why I Built This Integration

I've been running Home Assistant on my network for years, controlling everything from lights to climate systems through MQTT. The interface works fine, but I wanted something more natural—voice commands that actually understand context and intent, not just pattern-matched phrases.

Most voice assistants either lock you into their ecosystem or require brittle custom intents for every possible command variation. I wanted to describe what I wanted in plain language and have the system figure out the right MQTT messages to send.

When Anthropic released Claude Opus 4.5 with improved function calling, I saw an opportunity. I already had n8n running for automation workflows, and I knew I could bridge natural language to MQTT if the function calling was reliable enough.

My Setup

Here's what I'm actually running:

  • Home Assistant on a Proxmox VM, managing devices through MQTT
  • Mosquitto MQTT broker in a Docker container on the same host
  • n8n in Docker, handling the Claude API integration and MQTT publishing
  • Claude Opus 4.5 via Anthropic's API for natural language processing

The flow is straightforward: I send a text command to n8n, which passes it to Claude with function definitions for my devices. Claude returns structured function calls, and n8n translates those into MQTT messages that Home Assistant understands.

Device Schema in n8n

I defined my devices as Claude functions in n8n's HTTP Request node. Here's what one looks like:

{
  "name": "control_office_light",
  "description": "Control the office overhead light",
  "input_schema": {
    "type": "object",
    "properties": {
      "action": {
        "type": "string",
        "enum": ["on", "off", "toggle"],
        "description": "Action to perform"
      },
      "brightness": {
        "type": "integer",
        "minimum": 0,
        "maximum": 255,
        "description": "Brightness level (optional, only for 'on' action)"
      }
    },
    "required": ["action"]
  }
}

I created similar definitions for my living room lights, bedroom fan, and thermostat. Each function maps to a specific MQTT topic that Home Assistant subscribes to.

The n8n Workflow

My workflow has four nodes:

  1. Webhook trigger - Receives the natural language command via HTTP POST
  2. HTTP Request to Claude API - Sends the command with tool definitions
  3. Function node - Parses Claude's response and extracts tool calls
  4. MQTT Out node - Publishes the appropriate message to Mosquitto

The HTTP Request node configuration looks like this:

POST https://api.anthropic.com/v1/messages
Headers:
  x-api-key: [my API key]
  anthropic-version: 2023-06-01
  content-type: application/json

Body:
{
  "model": "claude-opus-4.5",
  "max_tokens": 1024,
  "tools": [
    // my device function definitions
  ],
  "messages": [
    {
      "role": "user",
      "content": "{{ $json.command }}"
    }
  ]
}

When Claude responds with a tool call, the Function node extracts the function name and parameters, then maps them to MQTT topics and payloads.

What Actually Works

The function calling is remarkably consistent. When I say "turn off the office light," Claude reliably calls control_office_light with {"action": "off"}. It handles variations like "kill the office lights" or "office light off" without additional training.

Context understanding exceeded my expectations. Commands like "make the living room brighter" correctly call the brightness function with an increased value, even though I didn't explicitly say a number. Claude infers reasonable defaults—usually bumping brightness by 50-70 points.

Multi-device commands work well: "turn off all the lights" generates multiple function calls in a single response. n8n processes each one sequentially, publishing to the appropriate MQTT topics.

MQTT Message Translation

The Function node converts Claude's structured output into Home Assistant-compatible MQTT messages. For the office light example:

// Claude returns:
{
  "name": "control_office_light",
  "input": {
    "action": "off"
  }
}

// Function node produces:
{
  "topic": "homeassistant/light/office/set",
  "payload": {
    "state": "OFF"
  }
}

For brightness control, it's slightly more complex:

// Claude returns:
{
  "name": "control_office_light",
  "input": {
    "action": "on",
    "brightness": 180
  }
}

// Function node produces:
{
  "topic": "homeassistant/light/office/set",
  "payload": {
    "state": "ON",
    "brightness": 180
  }
}

This mapping happens in JavaScript within the Function node, checking which device was called and building the appropriate MQTT structure.

What Didn't Work

State queries are problematic. I initially tried giving Claude a function to check device status, but it required round-tripping back through the API with the current state. That added latency and complexity without much benefit—most commands don't need to know current state.

I removed state checking and just send commands. If someone says "turn on the light" when it's already on, Home Assistant handles it gracefully. The extra API call wasn't worth it.

Ambiguous commands sometimes fail. "Make it warmer" works if I've only defined one thermostat function, but if I add heating for multiple rooms, Claude needs more context. I haven't solved this cleanly yet—right now I just use more specific commands.

Error handling in n8n is basic. If Claude returns an unexpected response format or the MQTT broker is down, the workflow just fails silently. I added a simple error output node that logs to a file, but I don't have retry logic or fallbacks yet.

Token costs add up faster than I expected. Each command uses 1,500-2,500 tokens because I'm sending all device function definitions every time. For frequent commands, this gets expensive. I'm considering caching the tool definitions in Claude's prompt cache, but I haven't tested whether that actually reduces costs in practice.

Function Definition Bloat

As I added more devices, the function definitions grew unwieldy. With 15+ devices, I'm sending ~8KB of JSON on every request. Claude handles it fine, but it feels inefficient.

I experimented with grouping devices—one "control_light" function with a "device_id" parameter instead of separate functions per device. Claude struggled with this. It would sometimes use the wrong device ID or forget which rooms had which devices.

Separate functions per device works better, even if it means more tokens. The explicit function names give Claude clearer context about what's available.

Real Latency Numbers

From webhook receipt to MQTT publish:

  • Simple command ("turn off office light"): 1.2-1.8 seconds
  • Multi-device command ("turn off all lights"): 1.5-2.3 seconds
  • Complex command ("set living room to 40% brightness"): 1.4-2.0 seconds

Most of this is Claude API response time. The MQTT publish is effectively instant once n8n has the structured output.

For comparison, my old pattern-matched voice commands responded in ~400ms. The extra second is noticeable but acceptable for the flexibility gained.

Key Takeaways

Claude's function calling is production-ready for this use case. I've run hundreds of commands over three weeks without a single malformed function call. The model consistently returns valid JSON that matches my schema definitions.

Natural language understanding is the real win here. I don't maintain intent lists or train on example phrases. New command variations just work because Claude understands the underlying intent.

MQTT is the right integration layer. It's simple, reliable, and Home Assistant already speaks it natively. No custom integrations or API wrappers needed—just publish to the right topic with the right payload.

Token costs matter at scale. For my 15-device setup, I'm using about 2,000 tokens per command. At current Opus 4.5 pricing, that's roughly $0.03 per command. Acceptable for personal use, but it would add up quickly in a high-traffic scenario.

n8n works well as the glue layer. The visual workflow makes debugging easy, and the built-in MQTT node handles connection management reliably. I initially considered writing a custom Python service, but n8n's HTTP and MQTT nodes saved me from dealing with connection pooling and error handling.

What I'd Do Differently

If I were starting over, I'd implement prompt caching from day one. My device function definitions rarely change, so they're perfect candidates for caching. That could cut token usage by 60-70% according to Anthropic's documentation.

I'd also add a simple state store—maybe Redis—to track recent commands and avoid redundant operations. Not for Claude to query, but for n8n to check before publishing to MQTT. If someone says "turn off the office light" twice in 10 seconds, the second command doesn't need to go through.

Finally, I'd separate device definitions into a configuration file instead of hardcoding them in the n8n workflow. Right now, adding a new device means editing the HTTP Request node's JSON. A separate config file would make that cleaner.

Current Limitations

This setup only handles commands, not queries. I can't ask "is the office light on?" and get a response. That would require bidirectional communication—Claude would need to call a status function, I'd need to query MQTT state topics, return that to Claude, and have it generate a natural language response.

Possible, but not implemented. For now, I just check the Home Assistant dashboard if I need to know device state.

No authentication or access control. Anyone who can reach my n8n webhook can control my devices. I'm running this on my internal network only, behind my firewall. If I exposed it externally, I'd need to add proper authentication.

No conversation history. Each command is stateless—Claude doesn't remember previous commands in the session. I can't say "turn on the office light" then "make it brighter" and have Claude know which light I mean. Every command must be self-contained.

I could implement conversation history by maintaining a message array and sending it with each request, but that would increase token usage significantly. Not worth it for my use case.

What This Enables

The real value isn't replacing simple on/off commands—those work fine with traditional voice assistants. It's handling the complex, contextual requests that would require dozens of custom intents in a traditional system.

Commands like "set the house to movie mode" can trigger multiple actions—dim living room lights to 20%, turn off office lights, set thermostat to 68°F—all from a single natural language input. Claude generates multiple function calls, n8n executes them in sequence, and the entire scene activates.

I'm also using this for time-based commands. "Turn on the porch light at sunset" works because Claude understands temporal context and can call scheduling functions (which I added later). I don't have to teach it what "sunset" means or how to calculate it—it just knows.

This pattern could extend beyond home automation. Any system that accepts structured commands via an API or message queue could work the same way. The natural language interface is just a translation layer to whatever protocol your system speaks.