{
  "name": "hass_control",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "hass_control",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "webhook",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        240,
        300
      ],
      "notes": "Control Home Assistant devices. Parameters: action (required: turn_on, turn_off, volume_up, volume_down, set_volume, mute, unmute, pause, play, next, previous), target (required: device name), value (optional: for set_volume 0-100)."
    },
    {
      "parameters": {
        "resource": "state",
        "operation": "getAll",
        "returnAll": true
      },
      "id": "get-states",
      "name": "Get All States",
      "type": "n8n-nodes-base.homeAssistant",
      "typeVersion": 1,
      "position": [
        460,
        300
      ],
      "credentials": {
        "homeAssistantApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const states = $input.all().map(i => i.json);\nconst body = $('Webhook').item.json.body || {};\nconst action = (body.action || '').toLowerCase();\nconst target = (body.target || '').toLowerCase();\nconst value = body.value;\n\nif (!action || !target) {\n  return { error: true, message: \"Missing action or target parameter.\" };\n}\n\n// Find matching entity by friendly_name (fuzzy match)\nlet match = null;\nlet matchScore = 0;\n\nfor (const entity of states) {\n  const friendlyName = (entity.attributes?.friendly_name || '').toLowerCase();\n  const entityId = entity.entity_id.toLowerCase();\n  \n  // Exact match on friendly name\n  if (friendlyName === target) {\n    match = entity;\n    matchScore = 100;\n    break;\n  }\n  \n  // Partial match on friendly name\n  if (friendlyName.includes(target) || target.includes(friendlyName)) {\n    const score = friendlyName.includes(target) ? 80 : 60;\n    if (score > matchScore) {\n      match = entity;\n      matchScore = score;\n    }\n  }\n  \n  // Match on entity_id\n  if (entityId.includes(target.replace(/\\s+/g, '_'))) {\n    if (50 > matchScore) {\n      match = entity;\n      matchScore = 50;\n    }\n  }\n}\n\nif (!match) {\n  // List available devices for helpful error\n  const available = states\n    .filter(e => ['switch', 'light', 'media_player', 'fan', 'cover'].includes(e.entity_id.split('.')[0]))\n    .map(e => e.attributes?.friendly_name || e.entity_id)\n    .slice(0, 10);\n  return { \n    error: true, \n    message: `Couldn't find \"${body.target}\". Available devices: ${available.join(', ')}.`\n  };\n}\n\n// Determine domain and service based on action\nconst domain = match.entity_id.split('.')[0];\nlet service = '';\nlet serviceData = { entity_id: match.entity_id };\n\nswitch (action) {\n  case 'turnon':\n  case 'turn_on':\n  case 'on':\n    service = 'turn_on';\n    break;\n  case 'turnoff':\n  case 'turn_off':\n  case 'off':\n    service = 'turn_off';\n    break;\n  case 'toggle':\n    service = 'toggle';\n    break;\n  case 'volumeup':\n  case 'volume_up':\n    service = 'volume_up';\n    break;\n  case 'volumedown':\n  case 'volume_down':\n    service = 'volume_down';\n    break;\n  case 'setvolume':\n  case 'set_volume':\n  case 'volume':\n    service = 'volume_set';\n    serviceData.volume_level = (value || 50) / 100;\n    break;\n  case 'mute':\n    service = 'volume_mute';\n    serviceData.is_volume_muted = true;\n    break;\n  case 'unmute':\n    service = 'volume_mute';\n    serviceData.is_volume_muted = false;\n    break;\n  case 'pause':\n    service = 'media_pause';\n    break;\n  case 'play':\n    service = 'media_play';\n    break;\n  case 'next':\n    service = 'media_next_track';\n    break;\n  case 'previous':\n  case 'prev':\n    service = 'media_previous_track';\n    break;\n  default:\n    return { error: true, message: `Unknown action \"${action}\". Use: TurnOn, TurnOff, VolumeUp, VolumeDown, SetVolume, Mute, Unmute, Pause, Play, Next, Previous.` };\n}\n\n// For switches, only turn_on/turn_off/toggle are valid\nif (domain === 'switch' && !['turn_on', 'turn_off', 'toggle'].includes(service)) {\n  return { error: true, message: `Can't ${action} a switch. Use TurnOn or TurnOff.` };\n}\n\nreturn {\n  error: false,\n  domain: domain,\n  service: service,\n  serviceData: serviceData,\n  friendlyName: match.attributes?.friendly_name || match.entity_id,\n  action: body.action\n};"
      },
      "id": "find-entity",
      "name": "Find Entity & Build Service",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        680,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "error-check",
              "leftValue": "={{ $json.error }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "if-error",
      "name": "Check Error",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        900,
        300
      ]
    },
    {
      "parameters": {
        "resource": "service",
        "operation": "call",
        "domain": "={{ $json.domain }}",
        "service": "={{ $json.service }}",
        "serviceAttributes": {
          "attributes": [
            {
              "name": "entity_id",
              "value": "={{ $json.serviceData.entity_id }}"
            },
            {
              "name": "volume_level",
              "value": "={{ $json.serviceData.volume_level }}"
            },
            {
              "name": "is_volume_muted",
              "value": "={{ $json.serviceData.is_volume_muted }}"
            }
          ]
        }
      },
      "id": "call-service",
      "name": "Call HA Service",
      "type": "n8n-nodes-base.homeAssistant",
      "typeVersion": 1,
      "position": [
        1120,
        200
      ],
      "credentials": {
        "homeAssistantApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const prev = $('Find Entity & Build Service').item.json;\nconst friendlyName = prev.friendlyName;\nconst action = prev.action;\n\n// Build natural response\nlet verb = '';\nswitch (action.toLowerCase()) {\n  case 'turnon':\n  case 'turn_on':\n  case 'on':\n    verb = 'Turned on';\n    break;\n  case 'turnoff':\n  case 'turn_off':\n  case 'off':\n    verb = 'Turned off';\n    break;\n  case 'toggle':\n    verb = 'Toggled';\n    break;\n  case 'volumeup':\n  case 'volume_up':\n    verb = 'Turned up volume on';\n    break;\n  case 'volumedown':\n  case 'volume_down':\n    verb = 'Turned down volume on';\n    break;\n  case 'setvolume':\n  case 'set_volume':\n  case 'volume':\n    verb = 'Set volume on';\n    break;\n  case 'mute':\n    verb = 'Muted';\n    break;\n  case 'unmute':\n    verb = 'Unmuted';\n    break;\n  case 'pause':\n    verb = 'Paused';\n    break;\n  case 'play':\n    verb = 'Playing';\n    break;\n  case 'next':\n    verb = 'Skipped to next on';\n    break;\n  case 'previous':\n  case 'prev':\n    verb = 'Went back on';\n    break;\n  default:\n    verb = action;\n}\n\nreturn {\n  message: `${verb} ${friendlyName}.`\n};"
      },
      "id": "format-success",
      "name": "Format Success",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1340,
        200
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ $json }}",
        "options": {}
      },
      "id": "respond-success",
      "name": "Respond Success",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1560,
        200
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ $json }}",
        "options": {}
      },
      "id": "respond-error",
      "name": "Respond Error",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1120,
        400
      ]
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Get All States",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get All States": {
      "main": [
        [
          {
            "node": "Find Entity & Build Service",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Entity & Build Service": {
      "main": [
        [
          {
            "node": "Check Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Error": {
      "main": [
        [
          {
            "node": "Respond Error",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Call HA Service",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call HA Service": {
      "main": [
        [
          {
            "node": "Format Success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Success": {
      "main": [
        [
          {
            "node": "Respond Success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "availableInMCP": true
  }
}