Skill: tmpbin/ycznwl-smarthome (version 1.0.6)

--- name: ycznwl-smarthome description: >- Create, edit, validate, and manage KNX Gateway automation workflows and scenes via REST API. Covers device listing, device control, scene CRUD/execution, workflow CRUD, import/export, trigger/node configuration, and workflow lifecycle (enable/disable/execute). Use when building or modifying automations, generating workflows for import, creating/executing scenes, or controlling devices programmatically. Use when the user says "create automation", "build workflow", "control device", "automation API", "create scene", or "execute scene". version: 1.0.6 compatibility: Requires network access to a user-provided KNX Gateway on a trusted LAN or VPN. metadata: openclaw: requires: env: - KNX_TOKEN primaryEnv: KNX_TOKEN always: false homepage: https://github.com/tmpbin/knx-gateway-skills --- # YCZNWL KNX Gateway Automation Agent Skill ## Connection | Parameter | Default | Description | |-----------|---------|-------------| | Base URL | `http://ycznwl.local/api/v1` | Default mDNS address — use as-is unless the user provides their own gateway address or IP | | Auth | `Authorization: Bearer ${KNX_TOKEN}` | See token instructions below | | Encoding | `UTF-8` | The gateway accepts and emits UTF-8 only. Always send `Content-Type: application/json; charset=utf-8` and decode responses as UTF-8. Names, descriptions, room names, action names, and any free-text fields may contain Chinese characters and **must** be transmitted/parsed as UTF-8. Do not URL-encode JSON payloads or wrap them in an extra encoding layer. | ### Getting an API Token 1. Open the KNX Gateway web UI 2. Click your avatar (top-right) → **Get API Token** 3. Copy the token **Safe token handling**: - Store the token in your platform's secret store or a local environment variable (`export KNX_TOKEN=...`) — never paste it directly into a chat conversation, as conversation logs may be persisted - Reference it in requests via the environment variable, not inline - Never hardcode the token in shared scripts or commit it to version control This skill's only required credential is `KNX_TOKEN`. Optional workflow nodes that send data outside the gateway or trusted LAN must be configured only after an explicit user request and endpoint review. Do not introduce hardcoded secrets into skill files or workflow examples. Default safety policy: keep automations local to the gateway and trusted LAN. Only author or enable off-LAN HTTP, MQTT, webhook, or email integrations when the user explicitly asks for that integration and approves the exact endpoint. ## Reference Documents This skill is organized into progressive reference files. Read them in order when you need detailed information: | File | Content | |------|---------| | [ref/devices.md](ref/devices.md) | Device types, subtypes, capabilities, actions, params | | [ref/scenes.md](ref/scenes.md) | **Scene data model, actions structure, KNX binding, execution behavior** | | [ref/triggers.md](ref/triggers.md) | Trigger types and their exact configuration (incl. `sun_event` with offset scheduling) | | [ref/nodes.md](ref/nodes.md) | Node types, subtypes (incl. `scene_exec`, `schedule_match` with sunrise/sunset boundaries), config structs, validation rules | | [ref/api.md](ref/api.md) | Complete REST API endpoint reference (devices, scenes, automation, system location/sun times) | | [ref/examples.md](ref/examples.md) | Realistic, validated workflow examples (incl. scene-based) | ## Quick Start — Agent Workflow ### Creating a new scene 1. **Discover devices**: `GET /devices` → note `uuid`, `name`, `type`, `capabilities`, and `room_id` (resolve via `GET /rooms`) 2. **Build actions array**: Each action targets a device: ```json { "id": "a1", "device_uuid": "light-uuid", "device_name": "Living Room Light", "action": "turn_off", "params": {}, "delay": 0 } ``` - `action` + `params` must match the device's capability (see [ref/devices.md](ref/devices.md)) - `delay` is in milliseconds (0 = immediate, 500 = 0.5s delay before this action) - `id` is a unique string per action (e.g. `"a1"`, `"a2"`) #### Scene authoring best practices - **Brightness implies power on (dimmable lights only)**: For lights with brightness control (`light_dimmer`, `light_tunable`, `light_rgb`), sending `set_brightness` with `brightness > 0` automatically powers the light on at the firmware/handler level. **Do not emit a separate `turn_on` action before `set_brightness`** — it adds latency and a redundant KNX telegram. Sending `set_brightness` with `brightness: 0` will likewise turn the light off. Use an explicit `turn_on` / `turn_off` only when the device has no brightness capability. - **Multi-room scenes — prefix `device_name` with the room name**: When a scene spans multiple rooms, prepend the room name to the action's display name so the execution log and UI are readable. Example: `"device_name": "客厅·主灯"` instead of just `"主灯"`. Single-room scenes may keep the bare device name. The room name comes from `GET /rooms` joined on `device.room_id`. This applies to both scene actions (`actions[].device_name`) and any `name` you set on automation `device_control` / `scene_exec` action nodes. - **Color temperature range**: Devices can be configured for any range within `1000–10000 K` with a configurable step. The backend clamps & step-aligns values automatically — but pass reasonable values that the user has actually configured for the device. 3. **Create**: `POST /scenes` with `name`, `actions`, optional `icon`, `color`, `description`, `enabled` 4. **Test**: `POST /scenes/:uuid/execute` → verify execution result ### Editing an existing scene 1. **Get current state**: `GET /scenes/:uuid` 2. **Update**: `PUT /scenes/:uuid` with partial fields: ```json { "name": "Updated Scene Name", "actions": [ ... ], "enabled": true } ``` Only include fields you want to change. The `actions` array replaces the existing one entirely. 3. **Test**: `POST /scenes/:uuid/execute` ### Managing scenes | Operation | Method | Endpoint | |-----------|--------|----------| | List all scenes | `GET` | `/scenes` (optional `?enabled=true`) | | Get scene | `GET` | `/scenes/:uuid` | | Create | `POST` | `/scenes` | | Update | `PUT` | `/scenes/:uuid` | | Delete | `DELETE` | `/scenes/:uuid` | | Execute | `POST` | `/scenes/:uuid/execute` | | Toggle enabled | `POST` | `/scenes/:uuid/toggle` | | Reorder | `PUT` | `/scenes/sort` | | View logs | `GET` | `/scenes/logs?scene_uuid=xxx` | See [ref/api.md](ref/api.md) for full request/response details. ### Using scenes in automations Scenes can be triggered from automation workflows in two ways: - **`scene_exec` node** (recommended): `{ "node_subtype": "scene_exec", "config": { "scene_uuid": "..." } }` - **`device_control` node** with virtual device: `{ "config": { "device_uuid": "scene-<scene_uuid>", "action": "activate_scene" } }` See [ref/examples.md](ref/examples.md) Examples 7 & 8 for complete workflow payloads. ### Creating a new automation 1. **Discover devices**: `GET /devices` → note `uuid`, `type`, `sub_type`, `capabilities` 2. **Discover scenes** (if needed): `GET /scenes` → note scene `uuid` for `scene_exec` nodes 3. **Build import JSON** using the format documented in [ref/api.md](ref/api.md) section "Import" 4. **Import**: `POST /automation/workflows/import` → returns new workflow with server-generated UUIDs 5. **Validate**: `POST /automation/workflows/:uuid/validate` 6. **Enable**: `POST /automation/workflows/:uuid/enable` ### Manually executing an automation — REQUIRES a manual trigger `POST /automation/workflows/:uuid/execute` can only fire **enabled** triggers of type `manual`. If the workflow has no manual trigger (e.g. only `device_event`, `cron`, `sun_event`, `knx_group_event`, or `webhook` triggers), the call returns `no enabled manual trigger found in workflow ...` and nothing executes. **Rule**: If the user wants the workflow to be runnable on demand from the UI or via the API (e.g. a "test" button, a chatbot command, a script), include at least one `{ "trigger_type": "manual", "enabled": true }` trigger in the import JSON. You may combine it with other triggers — they remain independent. ```json { "triggers": [ { "uuid": "t-manual", "trigger_type": "manual", "enabled": true, "config": {} }, { "uuid": "t-cron", "trigger_type": "cron", "enabled": true, "config": { "expression": "0 7 * * *" } } ] } ``` A workflow without a manual trigger still runs automatically when its other triggers fire — it just cannot be invoked through `POST /workflows/:uuid/execute`. ### Editing an existing automation 1. **Get current state**: `GET /automation/workflows/:uuid` 2. **Modify** triggers/nodes/edges — keep existing UUIDs for unchanged elements, generate new UUIDs for additions 3. **Save**: `PUT /automation/workflows/:uuid` 4. **Validate**: `POST /automation/workflows/:uuid/validate` ## Security Notes - **Imported workflows**: Always inspect imported workflow JSON before enabling. Look for unexpected off-LAN HTTP requests, MQTT brokers outside the trusted LAN, or webhook triggers exposed beyond the gateway network. Only import workflows from sources you trust. - **Network egress**: Prefer gateway-local and LAN-local workflows. HTTP request, MQTT publish, webhook, and email integrations are advanced gateway features; use them only when the user explicitly asks and the destination is reviewed. - **Webhook triggers**: Exposing a webhook URL can make the gateway reachable from outside its normal network. Use only when network exposure is deliberate and the token is kept secret. - **Base URL**: Verify the gateway address resolves to your local network before running any control commands. ## Critical Rules 1. **Device actions are NOT generic "set" or "toggle"**. Each device type has specific actions. Always check [ref/devices.md](ref/devices.md) for the exact action name and required params for each device type/subtype combination. 2. **UUIDs on import are placeholders**. The server remaps ALL UUIDs. Use simple placeholder values like `"t1"`, `"n1"`, `"e1"` — the server will replace them with real UUIDs. 3. **Imported workflows start in `draft` + `disabled` state**. Always validate then enable after import. 4. **Node configs are strictly typed**. A `device_control` node with wrong params will fail at execution time even if import succeeds. Always match action + params to the device's capability. 5. **Edge handle names**: Triggers use `source_handle: "output"`. Condition/logic nodes use `"true"` and `"false"` as source_handle. Action, delay, and transform nodes use `source_handle: "output"`. All target handles are `"input"`. 6. **`source_type` in edges**: Use `"trigger"` when source is a trigger UUID, use `"node"` when source is any node UUID. ## Common Pitfalls (MUST READ) ### Pitfall 1: `node_subtype` vs `sub_type` — DIFFERENT fields! - **Devices** use `sub_type` (e.g. `"sub_type": "light_dimmer"`) - **Nodes** use `node_subtype` (e.g. `"node_subtype": "device_control"`) - If you use `sub_type` in a node definition, it will be **silently ignored**. The node will have an empty subtype and validation will report "未知节点类型 action:" (unknown node type). **Always use `node_subtype` for nodes.** ### Pitfall 2: Validate response — check `errors` array, NOT just `valid` The `/validate` endpoint may return `"valid": true` even when `errors` is non-empty. Node config errors (e.g. missing subtype, wrong params) appear in `errors` but do not flip `valid` to false. **Always check the `errors` array is null or empty.** ``` ✗ BAD: if response.data.valid == true → proceed ✓ GOOD: if response.data.valid == true AND response.data.errors is null/empty → proceed ``` ### Pitfall 3: After import, verify the response data Always inspect the import response body. Confirm that: - `node_subtype` fields are non-empty for action nodes - `source_handle` / `target_handle` have the expected values - `config` objects contain the correct action/params If any field is missing or empty, the import JSON had a wrong field name. ## System Limits | Limit | Value | |-------|-------| | Max workflows | 200 | | Max nodes per flow | 50 | | Max edges per flow | 200 | | Max triggers per workflow | 5 | | Timeout range | 5–3600 seconds | | Max parallel | 10 | | Max retry count | 5 | | Max name length | 128 chars | | Max description length | 1024 chars | Retrieve current limits: `GET /automation/limits`

path: ref/api.md
content: # API Reference

Base URL: `http://ycznwl.local/api/v1` (default)

All requests require: `Authorization: Bearer <token>`

All responses use format:
```json
{ "code": 200, "data": { ... }, "message": "ok" }
```

Error responses:
```json
{ "code": 400, "data": null, "message": "error description" }
```

---

## Device Endpoints

### List All Devices

```
GET /devices
```

Returns **all** devices in a single response (no pagination). See [devices.md](devices.md) for response format.

### Get Single Device

```
GET /devices/:id
```

`:id` is the numeric device ID (not UUID).

### Control Device

```
POST /devices/:id/control
```

`:id` is the numeric device ID.

```json
{
  "action": "turn_on",
  "params": {}
}
```

See [devices.md](devices.md) for valid action/params combinations per device type.

---

## Scene Endpoints

Scenes are reusable groups of device actions. Each scene auto-creates a virtual device
(see [devices.md](devices.md) § Virtual Scene Devices).

### List Scenes

```
GET /scenes
```

Query params:
- `enabled` (string, optional) — `"true"` or `"false"` to filter

**Response**:
```json
{
  "scenes": [
    {
      "id": 1,
      "uuid": "a1b2c3d4-...",
      "name": "Good Night",
      "description": "Turn off all lights, close curtains",
      "icon": "moon",
      "color": "#6366F1",
      "actions": [
        {
          "id": "act1",
          "device_uuid": "light-uuid",
          "device_name": "Living Room Light",
          "action": "turn_off",
          "params": {},
          "delay": 0
        }
      ],
      "knx_bind_address": "",
      "knx_bind_dpt": "",
      "knx_scene_id": null,
      "device_uuid": "scene-a1b2c3d4-...",
      "enabled": true,
      "sort_order": 0,
      "exec_total": 42,
      "exec_success": 40,
      "exec_fail": 2,
      "last_exec_at": "2026-04-07T10:30:00Z",
      "created_at": "2026-03-01T08:00:00Z",
      "updated_at": "2026-04-07T10:30:00Z"
    }
  ],
  "total": 1
}
```

### Get Single Scene

```
GET /scenes/:uuid
```

### Create Scene

```
POST /scenes
```

```json
{
  "name": "Good Night",
  "description": "Turn off all lights",
  "icon": "moon",
  "color": "#6366F1",
  "actions": [
    {
      "id": "a1",
      "device_uuid": "light-uuid",
      "device_name": "Living Room Light",
      "action": "turn_off",
      "params": {},
      "delay": 0
    },
    {
      "id": "a2",
      "device_uuid": "curtain-uuid",
      "device_name": "Curtain",
      "action": "close",
      "params": {},
      "delay": 500
    }
  ],
  "enabled": true
}
```

Only `name` is required. Returns the scene object with server-generated `uuid` and
auto-created `device_uuid`.

**Optional KNX binding fields** (for scenes triggered by physical KNX panels):
- `knx_bind_address` — KNX group address (e.g. `"1/2/3"`)
- `knx_bind_dpt` — KNX DPT (e.g. `"18.001"`)
- `knx_scene_id` — KNX scene number (0–63)

### Update Scene

```
PUT /scenes/:uuid
```

Partial update — include only fields to change:
```json
{
  "name": "Updated Name",
  "enabled": false,
  "actions": [ ... ]
}
```

### Delete Scene

```
DELETE /scenes/:uuid
```

Also removes the associated virtual device.

### Execute Scene

```
POST /scenes/:uuid/execute
```

Runs all configured device actions. Returns execution result:
```json
{
  "success": true,
  "actions_executed": 3,
  "actions_failed": 0,
  "duration_ms": 150,
  "errors": []
}
```

Error tolerance: if an action fails, remaining actions still execute.

### Toggle Scene Enabled/Disabled

```
POST /scenes/:uuid/toggle
```

Returns:
```json
{ "uuid": "...", "enabled": false }
```

### Update Sort Order

```
PUT /scenes/sort
```

```json
{
  "order": [
    { "uuid": "scene-uuid-1", "sort_order": 0 },
    { "uuid": "scene-uuid-2", "sort_order": 1 }
  ]
}
```

### List Execution Logs

```
GET /scenes/logs?scene_uuid=xxx&limit=50&offset=0
```

Query params:
- `scene_uuid` (string, optional) — filter by scene
- `limit` (int, default 50)
- `offset` (int, default 0)

---

## Automation Endpoints

### Get System Limits

```
GET /automation/limits
```

Returns validation boundaries (max nodes, max timeout, etc.).

### List Workflows

```
GET /automation/workflows?limit=100&offset=0
```

Query params:
- `limit` (int, default 20, max 100)
- `offset` (int, default 0)
- `status` (string, optional) — filter by status: `draft`, `ready`, `active`, `error`

### Create Workflow (empty)

```
POST /automation/workflows
```

```json
{
  "name": "My Workflow",
  "description": "optional description"
}
```

Creates an empty workflow in `draft` state. Returns the full workflow object.

### Import Workflow (recommended for agents)

```
POST /automation/workflows/import
```

This is the **preferred** endpoint for agents creating workflows. It accepts a complete
workflow definition and handles UUID remapping, default value application, and validation.

**Request body**:
```json
{
  "schema_version": 1,
  "workflow": {
    "name": "Required — workflow name",
    "description": "Optional, default ''",
    "icon": "Optional, default 'zap'",
    "color": "Optional, default '#3B82F6'",
    "category": "Optional, default 'custom'",
    "tags": [],
    "exec_mode": "Optional: single|parallel|queue|restart, default 'single'",
    "max_parallel": 1,
    "debounce_ms": 0,
    "cooldown_ms": 0,
    "timeout_s": 300,
    "retry_max": 0,
    "retry_interval_s": 10,
    "flow_data": {
      "triggers": [ ... ],
      "nodes": [ ... ],
      "edges": [ ... ]
    }
  }
}
```

**Only `name` and `flow_data` (with at least one trigger) are required.**
All other fields use defaults when omitted.

**Server behavior**:
1. Remaps ALL UUIDs in triggers, nodes, and edges to new server-generated UUIDs
2. Preserves edge source/target relationships using a UUID mapping table
3. Applies default values for omitted optional fields
4. Creates workflow in `draft` status, `enabled = false`

**Response**: Full workflow object with server-assigned UUID and remapped flow_data.

> **Security — Review before enabling**: Before calling `/validate` and `/enable`, inspect
> the imported workflow JSON for off-LAN HTTP destinations, MQTT brokers outside the trusted
> LAN, webhook triggers, or embedded secrets. Only import workflows from sources you trust.

> **IMPORTANT — Verify import response**: After import, check the response body to confirm
> `node_subtype` is non-empty for all nodes. If it is empty `""`, you likely used the wrong
> field name (`sub_type` instead of `node_subtype`). Nodes use `node_subtype`; devices use
> `sub_type`. This is a common mistake.

> **IMPORTANT — Validate after import**: Always call `/validate` after import, and check
> BOTH `valid` AND `errors` fields. The endpoint may return `valid: true` with non-empty
> `errors` (e.g. for node config issues). Only proceed to enable if `errors` is null/empty.

### Get Workflow Detail

```
GET /automation/workflows/:uuid
```

Returns the complete workflow including flow_data with all triggers, nodes, and edges.

### Update Workflow

```
PUT /automation/workflows/:uuid
```

```json
{
  "workflow": {
    "name": "Updated Name",
    "timeout_s": 120
  },
  "triggers": [ ... ],
  "nodes": [ ... ],
  "edges": [ ... ]
}
```

The `workflow` object supports partial updates — only include fields you want to change.
`triggers`, `nodes`, `edges` are the complete new flow_data arrays (replacing existing).

**Important**: When editing, keep existing UUIDs for unchanged elements. Only generate
new UUIDs for newly added triggers/nodes/edges.

> **⚠ Update vs Import — different body structure**: The Update endpoint wraps
> `triggers`/`nodes`/`edges` at the top level alongside `workflow`. The Import endpoint
> nests them inside `workflow.flow_data`. Do not mix these formats — using the import
> structure in an update request will silently discard your flow changes.

### Delete Workflow

```
DELETE /automation/workflows/:uuid
```

### Clone Workflow

```
POST /automation/workflows/:uuid/clone
```

Creates a copy of the workflow with new UUIDs and "(Copy)" appended to the name.

### Validate Workflow

```
POST /automation/workflows/:uuid/validate
```

Runs server-side validation on the workflow's flow_data and configuration.

**Optional body** (for including config validation):
```json
{
  "timeout_s": 300,
  "exec_mode": "single",
  "max_parallel": 1
}
```

**Response**:
```json
{
  "valid": true,
  "errors": []
}
```

Or with errors:
```json
{
  "valid": false,
  "errors": [
    {
      "code": "trigger_config_incomplete",
      "message": "Device event trigger 'Temp' is missing device UUID",
      "node_uuid": "abc123",
      "field": "device_uuid"
    }
  ]
}
```

### Enable Workflow

```
POST /automation/workflows/:uuid/enable
```

Activates the workflow. Requires the workflow to pass validation.
Returns error if validation fails.

### Disable Workflow

```
POST /automation/workflows/:uuid/disable
```

Deactivates the workflow. Running executions are not cancelled.

### Execute Workflow (manual trigger)

```
POST /automation/workflows/:uuid/execute
```

Manually fires the workflow as if the manual trigger was activated.

---

## Execution Endpoints

### List Executions

```
GET /automation/executions?workflow_uuid=xxx&limit=20&offset=0
```

Query params:
- `workflow_uuid` (string, optional) — filter by workflow
- `status` (string, optional) — filter by status: `pending`, `running`, `completed`, `failed`, `cancelled`, `timeout`
- `limit` (int, default 20)
- `offset` (int, default 0)

### Get Execution Detail

```
GET /automation/executions/:uuid
```

### Get Execution Logs

```
GET /automation/executions/:uuid/logs
```

Returns per-node execution logs with timing information.

### Cancel Execution

```
POST /automation/executions/:uuid/cancel
```

Cancels a running execution.

---

## Sun Times Endpoints


### Get Today's Sun Times

```
GET /system/sun-times
```

Query params:
- `date` (string, optional) — Date in `YYYY-MM-DD` format. Default: today
- `tz` (string, optional) — Timezone override (e.g. `"Asia/Shanghai"`)

**Response**:
```json
{
  "latitude": 31.2304,
  "longitude": 121.4737,
  "date": "2025-04-24",
  "sunrise": "2025-04-24T05:23:00+08:00",
  "sunset": "2025-04-24T18:31:00+08:00",
  "timezone": "Asia/Shanghai"
}
```

---

## Edge Format

Edges connect triggers to nodes and nodes to nodes.

```json
{
  "uuid": "e1",
  "source_uuid": "t1",
  "source_type": "trigger",
  "source_handle": "output",
  "target_uuid": "n1",
  "target_handle": "input",
  "enabled": true
}
```

| Field | Description |
|-------|-------------|
| `source_uuid` | UUID of the source trigger or node |
| `source_type` | `"trigger"` if source is a trigger, `"node"` if source is a node |
| `source_handle` | `"output"` for triggers and action/delay/transform nodes. `"true"` or `"false"` for condition/logic nodes |
| `target_uuid` | UUID of the target node |
| `target_handle` | Always `"input"` |

### Edge routing rules

1. **Trigger → Node**: `source_type: "trigger"`, `source_handle: "output"`, `target_handle: "input"`
2. **Condition → Node (true branch)**: `source_type: "node"`, `source_handle: "true"`, `target_handle: "input"`
3. **Condition → Node (false branch)**: `source_type: "node"`, `source_handle: "false"`, `target_handle: "input"`
4. **Logic → Node (true branch)**: `source_type: "node"`, `source_handle: "true"`, `target_handle: "input"`
5. **Logic → Node (false branch)**: `source_type: "node"`, `source_handle: "false"`, `target_handle: "input"`
6. **Action → Node**: `source_type: "node"`, `source_handle: "output"`, `target_handle: "input"`
7. **Delay → Node**: `source_type: "node"`, `source_handle: "output"`, `target_handle: "input"`
8. **Transform → Node**: `source_type: "node"`, `source_handle: "output"`, `target_handle: "input"`


path: ref/devices.md
content: # Device Reference

## API

```
GET /api/v1/devices              — list all devices
GET /api/v1/devices/:id          — get single device (by numeric ID)
POST /api/v1/devices/:id/control — control a device
```

The control endpoint accepts:
```json
{ "action": "<action_name>", "params": { ... } }
```

## Device Types

| Type | Description | Common Subtypes |
|------|-------------|-----------------|
| `light` | Lighting | `light`, `light_dimmer`, `light_tunable`, `light_rgb` |
| `switch` | On/off switching | `switch`, `outlet`, `pulse` |
| `sensor` | Environmental/security sensors | See sensor subtypes below |
| `curtain` | Curtains and blinds | `curtain`, `blind`, `pulse_curtain` |
| `thermostat` | Temperature controllers | — |
| `ac` | Air conditioners | `ac`, `heater` |
| `fan` | Fans | `fan` |
| `garage` | Garage doors | `garage`, `pulse_garage` |
| `window` | Windows | `window`, `pulse_window` |
| `scene` |  scene controllers **and** virtual scenes | `scene` (check `protocol` to distinguish) |
| `lock` | Door locks | `lock` |
| `air_purifier` | Air purifiers | `air_purifier` |
| `player` | Media players | `player` |

### Sensor Subtypes

| Subtype | Measures | Status Property | Unit |
|---------|----------|-----------------|------|
| `sensor_temperature` | Temperature | `temperature` | °C |
| `sensor_humidity` | Humidity | `humidity` | % |
| `sensor_illuminance` | Light level | `illuminance` | lux |
| `sensor_co2` | CO₂ | `co2` | ppm |
| `sensor_co` | CO | `co` | ppm |
| `sensor_pm25` | PM2.5 | `pm25` | µg/m³ |
| `sensor_pm10` | PM10 | `pm10` | µg/m³ |
| `sensor_tvoc` | TVOC | `tvoc` | — |
| `sensor_air_quality` | Air quality index | `air_quality` | — |
| `sensor_motion` | Motion detected | `motion` | bool |
| `sensor_presence` | Presence detected | `presence` | bool |
| `sensor_smoke` | Smoke detected | `smoke` | bool |
| `sensor_leak` | Water leak | `leak` | bool |
| `sensor_contact` | Door/window contact | `contact` | bool |

## Capabilities, Actions, and Parameters

### on_off — Power control

**Applies to**: light, switch, outlet, ac, fan, air_purifier

| Action | Params | Description |
|--------|--------|-------------|
| `turn_on` | none | Turn the device on |
| `turn_off` | none | Turn the device off |
| `toggle` | none | Toggle current power state |

**Status property**: `power` (boolean)

**Example — turn on a light**:
```json
{ "action": "turn_on", "params": {} }
```

**Example — turn off a switch**:
```json
{ "action": "turn_off", "params": {} }
```

### brightness — Dimming

**Applies to**: light_dimmer, light_tunable, light_rgb

| Action | Params | Description |
|--------|--------|-------------|
| `set_brightness` | `{ "brightness": <0-100> }` | Set brightness percentage |

**Status property**: `brightness` (int, 0–100, unit: %)

**Example**:
```json
{ "action": "set_brightness", "params": { "brightness": 75 } }
```

### color_temperature — Color temperature

**Applies to**: light_tunable

| Action | Params | Description |
|--------|--------|-------------|
| `set_color_temperature` | `{ "color_temperature": <K> }` | Set color temp in Kelvin |

**Configurable per-device range**: any window within `1000–10000 K` (default `2700–6500 K`),
with a configurable step (default `100`). Out-of-range values are clamped and step-aligned
by the backend, but stay within the user's configured range when authoring scenes.

**Status property**: `color_temperature` (int, unit: K)

**Example**:
```json
{ "action": "set_color_temperature", "params": { "color_temperature": 4000 } }
```

### color — RGB color

**Applies to**: light_rgb

| Action | Params | Description |
|--------|--------|-------------|
| `set_color` | `{ "color": "#RRGGBB" }` | Set color as a hex string (preferred) |

**Examples**:
```json
{ "action": "set_color", "params": { "color": "#FF8800" } }
```

**Status properties**: `color` (string, e.g. `"#FF8800"`), `hue` (int, 0–360, read-only),
`saturation` (int, 0–100, read-only)

**Important — what `set_color` does NOT accept**:
- `hue` / `saturation` are **status properties only**. There is no `set_hue` or
  `set_saturation` action and they cannot be passed as params to `set_color`.
- HSV input is not supported. Convert HSV → hex before sending.

### open_close — Open/close control

**Applies to**: curtain, blind, garage, window, pulse_curtain, pulse_garage, pulse_window

| Action | Params | Description |
|--------|--------|-------------|
| `open` | none | Open the device |
| `close` | none | Close the device |
| `stop` | none | Stop current movement |

**Status properties**: `open_state` (bool), `is_opening` (bool), `is_closing` (bool)

**Example — open a curtain**:
```json
{ "action": "open", "params": {} }
```

### position — Position control

**Applies to**: curtain, blind

| Action | Params | Description |
|--------|--------|-------------|
| `set_position` | `{ "position": <0-100> }` | Set position percentage (0=closed, 100=open) |

**Status property**: `position` (int, 0–100, unit: %)

**Example — set curtain to 50%**:
```json
{ "action": "set_position", "params": { "position": 50 } }
```

### slat_position — Blind slat angle

**Applies to**: blind

| Action | Params | Description |
|--------|--------|-------------|
| `set_slat_position` | `{ "slat_position": <0-100> }` | Set slat angle percentage |

**Status property**: `slat_position` (int, 0–100, unit: %)

### target_temperature — Temperature setpoint

**Applies to**: thermostat, ac, heater

| Action | Params | Description |
|--------|--------|-------------|
| `set_target_temperature` | `{ "target_temperature": <0-100> }` | Set target temperature in °C |

**Status property**: `target_temperature` (float, 0–100, step: 0.5, unit: °C)

**Example — set AC to 24°C**:
```json
{ "action": "set_target_temperature", "params": { "target_temperature": 24 } }
```

### mode — Operation mode

**Applies to**: ac

| Action | Params | Description |
|--------|--------|-------------|
| `set_mode` | `{ "mode": "<mode>" }` | Set operation mode |

**Mode values**: `auto`, `cool`, `dry`, `fan_only`, `heat`

**Status property**: `mode` (enum)

**Example — set AC to cooling**:
```json
{ "action": "set_mode", "params": { "mode": "cool" } }
```

### fan_speed — Fan speed control

**Applies to**: ac, fan, air_purifier

| Action | Params | Description |
|--------|--------|-------------|
| `set_fan_speed` | `{ "fan_speed": <0-100> }` | Set fan speed percentage |
| `set_fan_speed_mode` | `{ "fan_speed_mode": "<mode>" }` | Set fan speed preset (enum value depends on device type) |

**`fan_speed_mode` enum values are NOT universal — they differ by device type:**

| Device type | Default enum | Notes |
|-------------|--------------|-------|
| `ac` | `auto`, `low`, `medium`, `high` | Can be customized per device via `fan_speed_mapping` |
| `fan` | depends on `speed_level_mapping` (typical defaults: `level1`, `level2`, `level3`) | Fans usually do NOT accept `auto`/`low`/`medium`/`high` directly |
| `air_purifier` | depends on `speed_level_mapping` | Same as fan — read device capability before sending |

> ⚠ Always read the target device's capability `fan_speed_mode` enum (via `GET /devices/:uuid` → `capabilities[].properties[name=fan_speed_mode].options`) before issuing `set_fan_speed_mode`. Sending an enum value that the device doesn't expose will be rejected by the backend (`unsupported fan_speed_mode: ...`).

**Status properties**: `fan_speed` (int, 0–100), `fan_speed_mode` (enum, varies per device)

### swing_mode — Swing/oscillation

**Applies to**: ac

| Action | Params | Description |
|--------|--------|-------------|
| `set_swing_mode` | `{ "swing_mode": true/false }` | Enable/disable swing |

**Status property**: `swing_mode` (bool)

### lock_unlock — Lock control

**Applies to**: lock

| Action | Params | Description |
|--------|--------|-------------|
| `lock` | none | Lock the device |
| `unlock` | none | Unlock the device |

**Status property**: `lock_state` (bool)

### scene — Scene activation

**Applies to**: scene (KNX physical scene controllers and gateway virtual scene devices)

| Action | Params | Description |
|--------|--------|-------------|
| `activate_scene` | none | Activate the scene (the scene number is fixed by device configuration; you don't pass it) |

### playback — Media playback

**Applies to**: player

| Action | Params | Description |
|--------|--------|-------------|
| `play` | none | Start playback |
| `pause` | none | Pause playback |
| `play_pause` | none | Toggle play/pause |
| `next_track` | none | Skip to next track (only if supported) |

**Status property**: `playing` (boolean)

### volume — Volume control

**Applies to**: player

| Action | Params | Description |
|--------|--------|-------------|
| `set_volume` | `{ "volume": <0-100> }` | Set volume percentage |
| `mute` | none | Mute (only if supported) |
| `unmute` | none | Unmute (only if supported) |
| `toggle_mute` | none | Toggle mute (only if supported) |

**Status properties**: `volume` (int, 0–100, unit: %), `muted` (boolean)

### playback_mode — Playback mode

**Applies to**: player (KNX only)

| Action | Params | Description |
|--------|--------|-------------|
| `set_playback_mode` | `{ "playback_mode": "<mode>" }` | Set playback mode |

**Mode values**: `sequential`, `repeat`, `repeat_one`, `shuffle`, `single`

### media_source — Media source selection

**Applies to**: player (KNX only)

| Action | Params | Description |
|--------|--------|-------------|
| `set_media_source` | `{ "media_source": "<source>" }` | Select media source |

**Source values**: `local`, `bluetooth`, `aux`, `usb`, `network`, `optical`, `hdmi`

## Read-only Capabilities (sensors)

These capabilities produce status properties but have no controllable actions:

| Capability | Property | Type | Unit | Range |
|------------|----------|------|------|-------|
| `temperature` | `temperature` | float | °C | -50 to 100 |
| `humidity` | `humidity` | float | % | 0 to 100 |
| `illuminance` | `illuminance` | float | lux | 0 to 100000 |
| `co2` | `co2` | float | ppm | 0 to 10000 |
| `co` | `co` | float | ppm | 0 to 10000 |
| `pm25` | `pm25` | float | µg/m³ | 0 to 1000 |
| `pm10` | `pm10` | float | µg/m³ | 0 to 1000 |
| `tvoc` | `tvoc` | float | — | 0 to 1000 |
| `air_quality` | `air_quality` | float | — | 0 to 1000 |
| `motion` | `motion` | bool | — | — |
| `presence` | `presence` | bool | — | — |
| `smoke` | `smoke` | bool | — | — |
| `leak` | `leak` | bool | — | — |
| `contact` | `contact` | bool | — | — |
| `battery` | `battery` (int), `low_battery` (bool) | int/bool | % | 0–100 |
| `running` | `running` | bool | — | — |
| `filter` | `filter` | int | % | 0–100 |

## Device Response Format

Each device in the API response looks like:

```json
{
  "id": "42",
  "uuid": "a1b2c3d4-...",
  "name": "Living Room Light",
  "type": "light",
  "sub_type": "light_dimmer",
  "protocol": "knx",
  "instance_name": "knx-main",
  "room_id": "3",
  "status": {
    "power": true,
    "brightness": 75
  },
  "is_online": true,
  "capabilities": ["on_off", "brightness"],
  "supported_actions": ["turn_on", "turn_off", "toggle", "set_brightness"],
  "properties": { ... },
  "config": { ... }
}
```

- `capabilities`: capability names (e.g. `["playback", "volume"]`)
- `supported_actions`: the actual actions this device supports — use this to determine which controls to show. Actions may vary even within the same device type depending on the underlying protocol and hardware.

Use `uuid` for automation trigger/node device references.
Use numeric `id` for the control API endpoint path.


path: ref/examples.md
content: # Workflow Examples

Complete, validated import payloads for common automation patterns.
All use placeholder UUIDs — the server remaps them on import.

Replace `DEVICE_UUID_*` placeholders with actual device UUIDs from `GET /devices`.

---

## Example 1: Motion-Activated Light

When motion is detected, turn on a light. When motion stops, turn it off.

```json
{
  "schema_version": 1,
  "workflow": {
    "name": "Motion Light",
    "description": "Turn on light when motion detected, off when cleared",
    "timeout_s": 60,
    "flow_data": {
      "triggers": [
        {
          "uuid": "t1",
          "trigger_type": "device_event",
          "label": "Motion Detected",
          "enabled": true,
          "device_uuid": "DEVICE_UUID_MOTION_SENSOR",
          "device_property": "motion",
          "compare_op": "changed",
          "compare_value": "",
          "pos_x": 100, "pos_y": 200
        }
      ],
      "nodes": [
        {
          "uuid": "n1",
          "node_type": "condition",
          "node_subtype": "device_state",
          "label": "Is Motion Active?",
          "config": {
            "device_uuid": "DEVICE_UUID_MOTION_SENSOR",
            "property": "motion",
            "operator": "is_true",
            "value": ""
          },
          "enabled": true,
          "pos_x": 350, "pos_y": 200
        },
        {
          "uuid": "n2",
          "node_type": "action",
          "node_subtype": "device_control",
          "label": "Turn On Light",
          "config": {
            "device_uuid": "DEVICE_UUID_LIGHT",
            "action": "turn_on",
            "params": {}
          },
          "enabled": true,
          "pos_x": 600, "pos_y": 100
        },
        {
          "uuid": "n3",
          "node_type": "action",
          "node_subtype": "device_control",
          "label": "Turn Off Light",
          "config": {
            "device_uuid": "DEVICE_UUID_LIGHT",
            "action": "turn_off",
            "params": {}
          },
          "enabled": true,
          "pos_x": 600, "pos_y": 300
        }
      ],
      "edges": [
        {
          "uuid": "e1",
          "source_uuid": "t1",
          "source_type": "trigger",
          "source_handle": "output",
          "target_uuid": "n1",
          "target_handle": "input",
          "enabled": true
        },
        {
          "uuid": "e2",
          "source_uuid": "n1",
          "source_type": "node",
          "source_handle": "true",
          "target_uuid": "n2",
          "target_handle": "input",
          "enabled": true
        },
        {
          "uuid": "e3",
          "source_uuid": "n1",
          "source_type": "node",
          "source_handle": "false",
          "target_uuid": "n3",
          "target_handle": "input",
          "enabled": true
        }
      ]
    }
  }
}
```

**Graph**: `trigger(motion changed) → condition(motion is_true?) → true: turn_on / false: turn_off`

---

## Example 2: Scheduled Curtain Control

Open curtains at 8 AM on weekdays, close at sunset.

```json
{
  "schema_version": 1,
  "workflow": {
    "name": "Weekday Curtain Schedule",
    "description": "Open curtains at 8 AM weekdays, close at sunset",
    "timeout_s": 60,
    "flow_data": {
      "triggers": [
        {
          "uuid": "t1",
          "trigger_type": "cron",
          "label": "8 AM Weekdays",
          "enabled": true,
          "cron_expr": "0 8 * * 1-5",
          "pos_x": 100, "pos_y": 100
        },
        {
          "uuid": "t2",
          "trigger_type": "sun_event",
          "label": "Sunset",
          "enabled": true,
          "sun_event": "sunset",
          "sun_offset_min": 0,
          "pos_x": 100, "pos_y": 350
        }
      ],
      "nodes": [
        {
          "uuid": "n1",
          "node_type": "action",
          "node_subtype": "device_control",
          "label": "Open Curtain",
          "config": {
            "device_uuid": "DEVICE_UUID_CURTAIN",
            "action": "open",
            "params": {}
          },
          "enabled": true,
          "pos_x": 400, "pos_y": 100
        },
        {
          "uuid": "n2",
          "node_type": "action",
          "node_subtype": "device_control",
          "label": "Close Curtain",
          "config": {
            "device_uuid": "DEVICE_UUID_CURTAIN",
            "action": "close",
            "params": {}
          },
          "enabled": true,
          "pos_x": 400, "pos_y": 350
        }
      ],
      "edges": [
        {
          "uuid": "e1",
          "source_uuid": "t1",
          "source_type": "trigger",
          "source_handle": "output",
          "target_uuid": "n1",
          "target_handle": "input",
          "enabled": true
        },
        {
          "uuid": "e2",
          "source_uuid": "t2",
          "source_type": "trigger",
          "source_handle": "output",
          "target_uuid": "n2",
          "target_handle": "input",
          "enabled": true
        }
      ]
    }
  }
}
```

**Graph**: `cron(8 AM weekdays) → open_curtain` | `sun_event(sunset) → close_curtain`

---

## Example 3: Temperature-Based AC Control with Notification

When temperature exceeds 28°C during business hours, turn on AC to cool mode at 24°C
and send a notification. When temperature drops below 22°C, turn off AC.

```json
{
  "schema_version": 1,
  "workflow": {
    "name": "Smart AC Control",
    "description": "Auto AC based on temperature during business hours",
    "timeout_s": 120,
    "cooldown_ms": 300000,
    "flow_data": {
      "triggers": [
        {
          "uuid": "t1",
          "trigger_type": "device_event",
          "label": "Temp Change",
          "enabled": true,
          "device_uuid": "DEVICE_UUID_TEMP_SENSOR",
          "device_property": "temperature",
          "compare_op": "changed",
          "compare_value": "",
          "pos_x": 100, "pos_y": 250
        }
      ],
      "nodes": [
        {
          "uuid": "n1",
          "node_type": "condition",
          "node_subtype": "schedule_match",
          "label": "Business Hours?",
          "config": {
            "days": [1, 2, 3, 4, 5],
            "from": "09:00",
            "to": "18:00",
            "timezone": "Asia/Shanghai"
          },
          "enabled": true,
          "pos_x": 300, "pos_y": 250
        },
        {
          "uuid": "n2",
          "node_type": "condition",
          "node_subtype": "device_state",
          "label": "Temp > 28°C?",
          "config": {
            "device_uuid": "DEVICE_UUID_TEMP_SENSOR",
            "property": "temperature",
            "operator": "gt",
            "value": "28"
          },
          "enabled": true,
          "pos_x": 550, "pos_y": 150
        },
        {
          "uuid": "n3",
          "node_type": "condition",
          "node_subtype": "device_state",
          "label": "Temp < 22°C?",
          "config": {
            "device_uuid": "DEVICE_UUID_TEMP_SENSOR",
            "property": "temperature",
            "operator": "lt",
            "value": "22"
          },
          "enabled": true,
          "pos_x": 550, "pos_y": 350
        },
        {
          "uuid": "n4",
          "node_type": "action",
          "node_subtype": "device_control",
          "label": "AC On + Cool",
          "config": {
            "device_uuid": "DEVICE_UUID_AC",
            "action": "turn_on",
            "params": {}
          },
          "enabled": true,
          "pos_x": 800, "pos_y": 50
        },
        {
          "uuid": "n5",
          "node_type": "action",
          "node_subtype": "device_control",
          "label": "Set Cool Mode",
          "config": {
            "device_uuid": "DEVICE_UUID_AC",
            "action": "set_mode",
            "params": { "mode": "cool" }
          },
          "enabled": true,
          "pos_x": 1050, "pos_y": 50
        },
        {
          "uuid": "n6",
          "node_type": "action",
          "node_subtype": "device_control",
          "label": "Set 24°C",
          "config": {
            "device_uuid": "DEVICE_UUID_AC",
            "action": "set_target_temperature",
            "params": { "target_temperature": 24 }
          },
          "enabled": true,
          "pos_x": 1300, "pos_y": 50
        },
        {
          "uuid": "n7",
          "node_type": "action",
          "node_subtype": "notify",
          "label": "Notify: AC On",
          "config": {
            "channel": "system",
            "body": "Temperature {{trigger.new_value}}°C — AC activated",
            "level": "info"
          },
          "enabled": true,
          "pos_x": 1300, "pos_y": 200
        },
        {
          "uuid": "n8",
          "node_type": "action",
          "node_subtype": "device_control",
          "label": "AC Off",
          "config": {
            "device_uuid": "DEVICE_UUID_AC",
            "action": "turn_off",
            "params": {}
          },
          "enabled": true,
          "pos_x": 800, "pos_y": 350
        }
      ],
      "edges": [
        {
          "uuid": "e1",
          "source_uuid": "t1",
          "source_type": "trigger",
          "source_handle": "output",
          "target_uuid": "n1",
          "target_handle": "input",
          "enabled": true
        },
        {
          "uuid": "e2",
          "source_uuid": "n1",
          "source_type": "node",
          "source_handle": "true",
          "target_uuid": "n2",
          "target_handle": "input",
          "enabled": true
        },
        {
          "uuid": "e3",
          "source_uuid": "n1",
          "source_type": "node",
          "source_handle": "true",
          "target_uuid": "n3",
          "target_handle": "input",
          "enabled": true
        },
        {
          "uuid": "e4",
          "source_uuid": "n2",
          "source_type": "node",
          "source_handle": "true",
          "target_uuid": "n4",
          "target_handle": "input",
          "enabled": true
        },
        {
          "uuid": "e5",
          "source_uuid": "n4",
          "source_type": "node",
          "source_handle": "output",
          "target_uuid": "n5",
          "target_handle": "input",
          "enabled": true
        },
        {
          "uuid": "e6",
          "source_uuid": "n5",
          "source_type": "node",
          "source_handle": "output",
          "target_uuid": "n6",
          "target_handle": "input",
          "enabled": true
        },
        {
          "uuid": "e7",
          "source_uuid": "n6",
          "source_type": "node",
          "source_handle": "output",
          "target_uuid": "n7",
          "target_handle": "input",
          "enabled": true
        },
        {
          "uuid": "e8",
          "source_uuid": "n3",
          "source_type": "node",
          "source_handle": "true",
          "target_uuid": "n8",
          "target_handle": "input",
          "enabled": true
        }
      ]
    }
  }
}
```

**Graph**:
```
trigger(temp changed)
  → condition(business hours?)
    → true: condition(temp > 28?)
      → true: turn_on_ac → set_cool → set_24c → notify
    → true: condition(temp < 22?)
      → true: turn_off_ac
```

---

## Example 4: KNX Switch Controls Light with Delay

When a KNX wall switch (DPT 1.001) is pressed ON, turn on a light.
When pressed OFF, wait 5 seconds then turn off the light.

```json
{
  "schema_version": 1,
  "workflow": {
    "name": "KNX Switch to Light with Delay Off",
    "timeout_s": 30,
    "flow_data": {
      "triggers": [
        {
          "uuid": "t1",
          "trigger_type": "knx_group_event",
          "label": "Wall Switch",
          "enabled": true,
          "knx_addresses": ["1/1/1"],
          "knx_event_types": ["write"],
          "knx_dpt": "1.001",
          "compare_op": "changed",
          "compare_value": "",
          "pos_x": 100, "pos_y": 200
        }
      ],
      "nodes": [
        {
          "uuid": "n1",
          "node_type": "condition",
          "node_subtype": "numeric_compare",
          "label": "Switch ON?",
          "config": {
            "left": { "source": "trigger", "value": "knx_value" },
            "operator": "eq",
            "right": { "source": "literal", "value": "1" }
          },
          "enabled": true,
          "pos_x": 350, "pos_y": 200
        },
        {
          "uuid": "n2",
          "node_type": "action",
          "node_subtype": "device_control",
          "label": "Light On",
          "config": {
            "device_uuid": "DEVICE_UUID_LIGHT",
            "action": "turn_on",
            "params": {}
          },
          "enabled": true,
          "pos_x": 600, "pos_y": 100
        },
        {
          "uuid": "n3",
          "node_type": "delay",
          "node_subtype": "fixed_delay",
          "label": "Wait 5s",
          "config": { "seconds": 5 },
          "enabled": true,
          "pos_x": 600, "pos_y": 300
        },
        {
          "uuid": "n4",
          "node_type": "action",
          "node_subtype": "device_control",
          "label": "Light Off",
          "config": {
            "device_uuid": "DEVICE_UUID_LIGHT",
            "action": "turn_off",
            "params": {}
          },
          "enabled": true,
          "pos_x": 850, "pos_y": 300
        }
      ],
      "edges": [
        {
          "uuid": "e1",
          "source_uuid": "t1",
          "source_type": "trigger",
          "source_handle": "output",
          "target_uuid": "n1",
          "target_handle": "input",
          "enabled": true
        },
        {
          "uuid": "e2",
          "source_uuid": "n1",
          "source_type": "node",
          "source_handle": "true",
          "target_uuid": "n2",
          "target_handle": "input",
          "enabled": true
        },
        {
          "uuid": "e3",
          "source_uuid": "n1",
          "source_type": "node",
          "source_handle": "false",
          "target_uuid": "n3",
          "target_handle": "input",
          "enabled": true
        },
        {
          "uuid": "e4",
          "source_uuid": "n3",
          "source_type": "node",
          "source_handle": "output",
          "target_uuid": "n4",
          "target_handle": "input",
          "enabled": true
        }
      ]
    }
  }
}
```

**Graph**: `knx_switch → condition(value==1?) → true: light_on / false: wait_5s → light_off`

---

## Example 5: MQTT Bridge with Value Mapping via Variable

Classify temperature as "critical" or "normal" using `set_variable`, then publish
to MQTT using `{{var.severity}}`. This demonstrates how variables let you set a value
in one branch and reference it in a shared downstream node.

```json
{
  "schema_version": 1,
  "workflow": {
    "name": "Temp to MQTT with Severity",
    "timeout_s": 30,
    "debounce_ms": 5000,
    "flow_data": {
      "triggers": [
        {
          "uuid": "t1",
          "trigger_type": "device_event",
          "label": "Temp Change",
          "enabled": true,
          "device_uuid": "DEVICE_UUID_TEMP_SENSOR",
          "device_property": "temperature",
          "compare_op": "changed",
          "compare_value": "",
          "pos_x": 100, "pos_y": 200
        }
      ],
      "nodes": [
        {
          "uuid": "n1",
          "node_type": "condition",
          "node_subtype": "device_state",
          "label": "Temp > 30?",
          "config": {
            "device_uuid": "DEVICE_UUID_TEMP_SENSOR",
            "property": "temperature",
            "operator": "gt",
            "value": "30"
          },
          "enabled": true,
          "pos_x": 350, "pos_y": 200
        },
        {
          "uuid": "n2",
          "node_type": "action",
          "node_subtype": "set_variable",
          "label": "Set: Critical",
          "config": {
            "key": "severity",
            "value": { "source": "literal", "value": "critical" }
          },
          "enabled": true,
          "pos_x": 600, "pos_y": 100
        },
        {
          "uuid": "n3",
          "node_type": "action",
          "node_subtype": "set_variable",
          "label": "Set: Normal",
          "config": {
            "key": "severity",
            "value": { "source": "literal", "value": "normal" }
          },
          "enabled": true,
          "pos_x": 600, "pos_y": 300
        },
        {
          "uuid": "n4",
          "node_type": "action",
          "node_subtype": "mqtt_publish",
          "label": "Publish Temp (Critical)",
          "config": {
            "topic": "home/temperature/status",
            "payload": "{\"temp\": {{trigger.new_value}}, \"severity\": \"{{var.severity}}\"}",
            "qos": 1
          },
          "enabled": true,
          "pos_x": 900, "pos_y": 100
        },
        {
          "uuid": "n5",
          "node_type": "action",
          "node_subtype": "mqtt_publish",
          "label": "Publish Temp (Normal)",
          "config": {
            "topic": "home/temperature/status",
            "payload": "{\"temp\": {{trigger.new_value}}, \"severity\": \"{{var.severity}}\"}",
            "qos": 0
          },
          "enabled": true,
          "pos_x": 900, "pos_y": 300
        }
      ],
      "edges": [
        {
          "uuid": "e1",
          "source_uuid": "t1",
          "source_type": "trigger",
          "source_handle": "output",
          "target_uuid": "n1",
          "target_handle": "input",
          "enabled": true
        },
        {
          "uuid": "e2",
          "source_uuid": "n1",
          "source_type": "node",
          "source_handle": "true",
          "target_uuid": "n2",
          "target_handle": "input",
          "enabled": true
        },
        {
          "uuid": "e3",
          "source_uuid": "n1",
          "source_type": "node",
          "source_handle": "false",
          "target_uuid": "n3",
          "target_handle": "input",
          "enabled": true
        },
        {
          "uuid": "e4",
          "source_uuid": "n2",
          "source_type": "node",
          "source_handle": "output",
          "target_uuid": "n4",
          "target_handle": "input",
          "enabled": true
        },
        {
          "uuid": "e5",
          "source_uuid": "n3",
          "source_type": "node",
          "source_handle": "output",
          "target_uuid": "n5",
          "target_handle": "input",
          "enabled": true
        }
      ]
    }
  }
}
```

**Graph**: `trigger(temp changed) → condition(temp > 30?) → true: set(severity=critical) → mqtt` / `false: set(severity=normal) → mqtt`

Both branches publish to the same topic with `{{var.severity}}` — the variable carries the branch result into the payload.

---

## Example 6: Multi-Condition with Logic AND

Turn on a fan only when temperature > 26°C AND humidity > 70%.

```json
{
  "schema_version": 1,
  "workflow": {
    "name": "Hot & Humid Fan Control",
    "timeout_s": 60,
    "cooldown_ms": 60000,
    "flow_data": {
      "triggers": [
        {
          "uuid": "t1",
          "trigger_type": "device_event",
          "label": "Temp or Humidity Change",
          "enabled": true,
          "device_uuid": "DEVICE_UUID_TEMP_SENSOR",
          "device_property": "temperature",
          "compare_op": "changed",
          "compare_value": "",
          "pos_x": 100, "pos_y": 200
        }
      ],
      "nodes": [
        {
          "uuid": "n1",
          "node_type": "condition",
          "node_subtype": "device_state",
          "label": "Temp > 26°C?",
          "config": {
            "device_uuid": "DEVICE_UUID_TEMP_SENSOR",
            "property": "temperature",
            "operator": "gt",
            "value": "26"
          },
          "enabled": true,
          "pos_x": 350, "pos_y": 100
        },
        {
          "uuid": "n2",
          "node_type": "condition",
          "node_subtype": "device_state",
          "label": "Humidity > 70%?",
          "config": {
            "device_uuid": "DEVICE_UUID_HUMIDITY_SENSOR",
            "property": "humidity",
            "operator": "gt",
            "value": "70"
          },
          "enabled": true,
          "pos_x": 350, "pos_y": 300
        },
        {
          "uuid": "n3",
          "node_type": "logic",
          "node_subtype": "and",
          "label": "Both True?",
          "config": {},
          "enabled": true,
          "pos_x": 600, "pos_y": 200
        },
        {
          "uuid": "n4",
          "node_type": "action",
          "node_subtype": "device_control",
          "label": "Fan On",
          "config": {
            "device_uuid": "DEVICE_UUID_FAN",
            "action": "turn_on",
            "params": {}
          },
          "enabled": true,
          "pos_x": 850, "pos_y": 100
        },
        {
          "uuid": "n5",
          "node_type": "action",
          "node_subtype": "device_control",
          "label": "Fan Off",
          "config": {
            "device_uuid": "DEVICE_UUID_FAN",
            "action": "turn_off",
            "params": {}
          },
          "enabled": true,
          "pos_x": 850, "pos_y": 300
        }
      ],
      "edges": [
        {
          "uuid": "e1",
          "source_uuid": "t1",
          "source_type": "trigger",
          "source_handle": "output",
          "target_uuid": "n1",
          "target_handle": "input",
          "enabled": true
        },
        {
          "uuid": "e2",
          "source_uuid": "t1",
          "source_type": "trigger",
          "source_handle": "output",
          "target_uuid": "n2",
          "target_handle": "input",
          "enabled": true
        },
        {
          "uuid": "e3",
          "source_uuid": "n1",
          "source_type": "node",
          "source_handle": "true",
          "target_uuid": "n3",
          "target_handle": "input",
          "enabled": true
        },
        {
          "uuid": "e4",
          "source_uuid": "n2",
          "source_type": "node",
          "source_handle": "true",
          "target_uuid": "n3",
          "target_handle": "input",
          "enabled": true
        },
        {
          "uuid": "e5",
          "source_uuid": "n3",
          "source_type": "node",
          "source_handle": "true",
          "target_uuid": "n4",
          "target_handle": "input",
          "enabled": true
        },
        {
          "uuid": "e6",
          "source_uuid": "n3",
          "source_type": "node",
          "source_handle": "false",
          "target_uuid": "n5",
          "target_handle": "input",
          "enabled": true
        }
      ]
    }
  }
}
```

**Graph**:
```
trigger(temp changed) ─┬→ condition(temp > 26?) ──true──→ AND ──true──→ fan_on
                        └→ condition(humidity > 70?) ──true──↗    └──false──→ fan_off
```

**Note**: The `AND` logic node correctly receives edges from two condition nodes (not
directly from the trigger). This satisfies the validation rule that logic nodes must have
condition nodes as inputs.

**Important — single trigger limitation**: This workflow only fires on temperature
changes. A humidity spike alone will not trigger it (the humidity condition reads the
current snapshot value). If you also need humidity changes to fire the workflow, add a
second `device_event` trigger watching `DEVICE_UUID_HUMIDITY_SENSOR / humidity / changed`
and connect it to both condition nodes the same way.

---

## Example 7: Scheduled Scene Execution (Good Night / Good Morning)

Execute a "Good Night" scene at 11 PM and a "Good Morning" scene at 7 AM.
This uses the `scene_exec` action node to trigger pre-configured scenes.

First, find scene UUIDs via `GET /scenes`.

```json
{
  "schema_version": 1,
  "workflow": {
    "name": "Daily Scene Schedule",
    "description": "Good Night at 11 PM, Good Morning at 7 AM",
    "timeout_s": 120,
    "flow_data": {
      "triggers": [
        {
          "uuid": "t1",
          "trigger_type": "cron",
          "label": "11 PM",
          "enabled": true,
          "cron_expr": "0 23 * * *",
          "pos_x": 100, "pos_y": 100
        },
        {
          "uuid": "t2",
          "trigger_type": "cron",
          "label": "7 AM",
          "enabled": true,
          "cron_expr": "0 7 * * *",
          "pos_x": 100, "pos_y": 350
        }
      ],
      "nodes": [
        {
          "uuid": "n1",
          "node_type": "action",
          "node_subtype": "scene_exec",
          "label": "Good Night Scene",
          "config": {
            "scene_uuid": "SCENE_UUID_GOOD_NIGHT"
          },
          "enabled": true,
          "pos_x": 400, "pos_y": 100
        },
        {
          "uuid": "n2",
          "node_type": "action",
          "node_subtype": "scene_exec",
          "label": "Good Morning Scene",
          "config": {
            "scene_uuid": "SCENE_UUID_GOOD_MORNING"
          },
          "enabled": true,
          "pos_x": 400, "pos_y": 350
        },
        {
          "uuid": "n3",
          "node_type": "action",
          "node_subtype": "notify",
          "label": "Notify: Night",
          "config": {
            "channel": "system",
            "body": "Good Night scene activated",
            "level": "info"
          },
          "enabled": true,
          "pos_x": 650, "pos_y": 100
        }
      ],
      "edges": [
        {
          "uuid": "e1",
          "source_uuid": "t1",
          "source_type": "trigger",
          "source_handle": "output",
          "target_uuid": "n1",
          "target_handle": "input",
          "enabled": true
        },
        {
          "uuid": "e2",
          "source_uuid": "t2",
          "source_type": "trigger",
          "source_handle": "output",
          "target_uuid": "n2",
          "target_handle": "input",
          "enabled": true
        },
        {
          "uuid": "e3",
          "source_uuid": "n1",
          "source_type": "node",
          "source_handle": "output",
          "target_uuid": "n3",
          "target_handle": "input",
          "enabled": true
        }
      ]
    }
  }
}
```

**Graph**: `cron(11 PM) → scene_exec(Good Night) → notify` | `cron(7 AM) → scene_exec(Good Morning)`

---

## Example 8: Motion Triggers Scene via Virtual Device

When motion is detected, execute a "Welcome Home" scene by controlling the
virtual scene device (alternative to `scene_exec` — using `device_control`
with a virtual scene device UUID).

```json
{
  "schema_version": 1,
  "workflow": {
    "name": "Motion Welcome Scene",
    "description": "Execute Welcome Home scene when motion detected",
    "timeout_s": 60,
    "cooldown_ms": 60000,
    "flow_data": {
      "triggers": [
        {
          "uuid": "t1",
          "trigger_type": "device_event",
          "label": "Motion Detected",
          "enabled": true,
          "device_uuid": "DEVICE_UUID_MOTION_SENSOR",
          "device_property": "motion",
          "compare_op": "is_true",
          "compare_value": "",
          "pos_x": 100, "pos_y": 200
        }
      ],
      "nodes": [
        {
          "uuid": "n1",
          "node_type": "action",
          "node_subtype": "device_control",
          "label": "Execute Welcome Scene",
          "config": {
            "device_uuid": "scene-SCENE_UUID_WELCOME",
            "action": "activate_scene",
            "params": {}
          },
          "enabled": true,
          "pos_x": 400, "pos_y": 200
        }
      ],
      "edges": [
        {
          "uuid": "e1",
          "source_uuid": "t1",
          "source_type": "trigger",
          "source_handle": "output",
          "target_uuid": "n1",
          "target_handle": "input",
          "enabled": true
        }
      ]
    }
  }
}
```

**Note**: The `device_uuid` uses the virtual device UUID pattern `scene-<scene_uuid>`.
This approach works identically to `scene_exec` but goes through the `device_control`
path. Prefer `scene_exec` for clarity in most cases.

---

## Example 9: KNX Send — Mirror Gateway Light State to KNX Bus

When the gateway changes a light's power state (via app, automation, etc.), write the
new value back to a KNX group address so physical KNX panels stay in sync.

```json
{
  "schema_version": 1,
  "workflow": {
    "name": "Mirror Light State to KNX",
    "description": "Sync gateway light state back to KNX group address",
    "timeout_s": 30,
    "flow_data": {
      "triggers": [
        {
          "uuid": "t1",
          "trigger_type": "device_event",
          "label": "Light Power Changed",
          "enabled": true,
          "device_uuid": "DEVICE_UUID_LIGHT",
          "device_property": "power",
          "compare_op": "changed",
          "compare_value": "",
          "pos_x": 100, "pos_y": 200
        }
      ],
      "nodes": [
        {
          "uuid": "n1",
          "node_type": "action",
          "node_subtype": "knx_send",
          "label": "Write State to KNX Bus",
          "config": {
            "targets": [
              {
                "address": "1/1/1",
                "command_type": "write",
                "dpt": "1.001",
                "value": { "source": "trigger", "value": "new_value" }
              }
            ]
          },
          "enabled": true,
          "pos_x": 400, "pos_y": 200
        }
      ],
      "edges": [
        {
          "uuid": "e1",
          "source_uuid": "t1",
          "source_type": "trigger",
          "source_handle": "output",
          "target_uuid": "n1",
          "target_handle": "input",
          "enabled": true
        }
      ]
    }
  }
}
```

**Graph**: `trigger(light power changed) → knx_send(write new_value to 1/1/1 as DPT 1.001)`

**Key point**: `value: { "source": "trigger", "value": "new_value" }` passes the trigger's
new value directly into the KNX telegram — no condition node needed when you want to forward
every change unconditionally.

To send multiple addresses at once (e.g. status feedback + scene recall), add more objects
to the `targets` array within the same `knx_send` node.

---

## Example 10: HTTP Request — Notify LAN Service on Smoke Alert

When smoke is detected, send a local system notification and notify a reviewed LAN service
(for example a local security panel, local notification service, or Home Assistant instance).
Use off-LAN endpoints only when the user explicitly asks for them and approves the exact URL.

```json
{
  "schema_version": 1,
  "workflow": {
    "name": "Smoke Alert LAN Notify",
    "description": "System notify + LAN HTTP call when smoke sensor fires",
    "timeout_s": 30,
    "cooldown_ms": 300000,
    "flow_data": {
      "triggers": [
        {
          "uuid": "t1",
          "trigger_type": "device_event",
          "label": "Smoke Detected",
          "enabled": true,
          "device_uuid": "DEVICE_UUID_SMOKE_SENSOR",
          "device_property": "smoke",
          "compare_op": "is_true",
          "compare_value": "",
          "pos_x": 100, "pos_y": 200
        }
      ],
      "nodes": [
        {
          "uuid": "n1",
          "node_type": "action",
          "node_subtype": "notify",
          "label": "System Alert",
          "config": {
            "channel": "system",
            "body": "Smoke detected by {{trigger.device_uuid}}",
            "level": "error"
          },
          "enabled": true,
          "pos_x": 400, "pos_y": 100
        },
        {
          "uuid": "n2",
          "node_type": "action",
          "node_subtype": "http_request",
          "label": "Notify LAN Service",
          "config": {
            "method": "POST",
            "url": "http://lan-notifier.local/api/alerts",
            "headers": {
              "Content-Type": "application/json"
            },
            "body": "{\"event\": \"smoke\", \"device\": \"{{trigger.device_uuid}}\", \"value\": {{trigger.new_value}}}",
            "timeout_s": 10
          },
          "enabled": true,
          "pos_x": 400, "pos_y": 300
        }
      ],
      "edges": [
        {
          "uuid": "e1",
          "source_uuid": "t1",
          "source_type": "trigger",
          "source_handle": "output",
          "target_uuid": "n1",
          "target_handle": "input",
          "enabled": true
        },
        {
          "uuid": "e2",
          "source_uuid": "t1",
          "source_type": "trigger",
          "source_handle": "output",
          "target_uuid": "n2",
          "target_handle": "input",
          "enabled": true
        }
      ]
    }
  }
}
```

**Graph**: `trigger(smoke=true) ─┬→ notify(system error)` / `└→ http_request(POST to LAN endpoint)`

Both nodes receive the trigger output directly — they run in parallel (both are in separate
branches from the same trigger). `cooldown_ms: 300000` prevents repeated alerts within 5 minutes.

**Template note**: In JSON body strings, string values need quotes around the template
(`"{{trigger.device_uuid}}"`), but numeric values do not (`{{trigger.new_value}}`).
Make sure the JSON stays valid after template substitution.


path: ref/nodes.md
content: # Node Reference

Nodes are the processing elements of a workflow. Each node lives in `flow_data.nodes[]`.

> **CRITICAL**: The node subtype field is `"node_subtype"`, NOT `"sub_type"`.
> Devices use `sub_type`, but nodes use `node_subtype`. Mixing them up will silently
> drop the subtype, causing validation failures like "未知节点类型 action:".

## Common Node Fields

```json
{
  "uuid": "placeholder-remapped-on-import",
  "node_type": "<type>",
  "node_subtype": "<subtype>",
  "label": "Human-readable name",
  "config": { ... },
  "enabled": true,
  "pos_x": 400,
  "pos_y": 200,
  "width": 0,
  "collapsed": false,
  "notes": ""
}
```

Only `uuid`, `node_type`, `node_subtype`, `label`, `config`, `enabled`, `pos_x`, `pos_y`
are required for import.

## ValueRef Structure

Several node configs use `ValueRef` to reference dynamic values:

```json
{
  "source": "<source_type>",
  "value": "<reference>"
}
```

| Source | Value format | Description |
|--------|-------------|-------------|
| `literal` | Any string | A hardcoded value, e.g. `"25"`, `"hello"` |
| `trigger` | Property path | Reference to trigger data, e.g. `"new_value"`, `"knx_value"` |
| `device` | `<uuid>.<property>` | Current value of a device property, e.g. `"abc123.temperature"` |
| `node_output` | `<node_uuid>` | Output of a previously executed node — **requires the server-assigned UUID**; only usable when editing an existing workflow (get UUIDs via `GET /automation/workflows/:uuid`), not in import payloads where UUIDs are placeholders |
| `variable` | `<key>` | A workflow variable set by `set_variable` node |

---

## Condition Nodes (`node_type: "condition"`)

Condition nodes evaluate to `true` or `false`. They have two output handles.

### device_state — Check Device Property

Checks the current value of a device property against a threshold.

**Config (`CfgDeviceState`)**:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `device_uuid` | string | yes | UUID of the device |
| `property` | string | yes | Property to check (e.g. `power`, `temperature`, `brightness`) |
| `operator` | string | yes | Compare operator: `eq`, `ne`, `gt`, `lt`, `gte`, `lte`, `is_true`, `is_false` |
| `value` | string | depends | Compare threshold (not needed for `is_true`/`is_false`) |

**Output handles**: `true`, `false`

**Example — is light on?**:
```json
{
  "uuid": "n1",
  "node_type": "condition",
  "node_subtype": "device_state",
  "label": "Is Light On",
  "config": {
    "device_uuid": "light-uuid",
    "property": "power",
    "operator": "is_true",
    "value": ""
  },
  "enabled": true,
  "pos_x": 300, "pos_y": 200
}
```

**Example — temperature above 26°C?**:
```json
{
  "config": {
    "device_uuid": "temp-sensor-uuid",
    "property": "temperature",
    "operator": "gt",
    "value": "26"
  }
}
```

### numeric_compare — Compare Two Values

Compares two dynamic values using ValueRef.

**Config (`CfgNumericCompare`)**:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `left` | ValueRef | yes | Left operand |
| `operator` | string | yes | `eq`, `ne`, `gt`, `lt`, `gte`, `lte` |
| `right` | ValueRef | yes | Right operand |

**Output handles**: `true`, `false`

**Example — trigger value > 50?**:
```json
{
  "config": {
    "left": { "source": "trigger", "value": "new_value" },
    "operator": "gt",
    "right": { "source": "literal", "value": "50" }
  }
}
```

### schedule_match — Time Window Check

Checks if the current time falls within a specified schedule. Each boundary (from/to)
can independently be a fixed clock time or a sun event (sunrise/sunset) with an offset.

**Config (`CfgScheduleMatch`)**:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `days` | int[] | no | Days of week: 0=Sun, 1=Mon, ..., 6=Sat. Empty = all days |
| `from_type` | string | no | `"fixed"`, `"sunrise"`, or `"sunset"`. Default: `"fixed"` |
| `from` | string | no | Start time `"HH:MM"` (24h). Only used when `from_type` is `"fixed"`. Empty = 00:00 |
| `from_offset_min` | int | no | Offset in minutes for sun events (-720 to 720). Negative = before, positive = after |
| `to_type` | string | no | `"fixed"`, `"sunrise"`, or `"sunset"`. Default: `"fixed"` |
| `to` | string | no | End time `"HH:MM"` (24h). Only used when `to_type` is `"fixed"`. Empty = 23:59 |
| `to_offset_min` | int | no | Offset in minutes for sun events (-720 to 720) |
| `timezone` | string | no | Timezone, e.g. `"Asia/Shanghai"`. Empty = system default |

**Output handles**: `true`, `false`

**Note**: Sun-based boundaries require system geographic location to be configured
(via Settings → Location Config). If location is not configured, validation will report
a `system_location` error. Cross-midnight windows (e.g. sunset → sunrise) are supported.

**Example — weekdays 9AM to 6PM (fixed)**:
```json
{
  "config": {
    "days": [1, 2, 3, 4, 5],
    "from_type": "fixed",
    "from": "09:00",
    "to_type": "fixed",
    "to": "18:00"
  }
}
```

**Example — sunrise to sunset (daytime only)**:
```json
{
  "config": {
    "from_type": "sunrise",
    "to_type": "sunset"
  }
}
```

**Example — 30 min before sunset to sunrise (night mode with offset)**:
```json
{
  "config": {
    "from_type": "sunset",
    "from_offset_min": -30,
    "to_type": "sunrise"
  }
}
```

**Example — mixed: fixed start, end at sunset**:
```json
{
  "config": {
    "from_type": "fixed",
    "from": "08:00",
    "to_type": "sunset",
    "to_offset_min": -15
  }
}
```

---

## Action Nodes (`node_type: "action"`)

Action nodes perform operations. They have one output handle: `output`.

### device_control — Control a Device

Sends a control command to a device.

**Config (`CfgDeviceControl`)**:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `device_uuid` | string | yes | UUID of the device to control |
| `action` | string | yes | Action name (see [devices.md](devices.md) for valid actions per device type) |
| `params` | object | no | Action parameters (see [devices.md](devices.md) for params per action) |

**CRITICAL**: The `action` and `params` must exactly match the device's capability.
Do NOT use generic names like `"set"` or `"switch"`. Use the exact action names from
[devices.md](devices.md).

**Example — turn on a light**:
```json
{
  "uuid": "n1",
  "node_type": "action",
  "node_subtype": "device_control",
  "label": "Turn On Light",
  "config": {
    "device_uuid": "light-uuid-here",
    "action": "turn_on",
    "params": {}
  },
  "enabled": true,
  "pos_x": 500, "pos_y": 200
}
```

**Example — set brightness to 50%**:
```json
{
  "config": {
    "device_uuid": "dimmer-uuid",
    "action": "set_brightness",
    "params": { "brightness": 50 }
  }
}
```

**Example — close curtain**:
```json
{
  "config": {
    "device_uuid": "curtain-uuid",
    "action": "close",
    "params": {}
  }
}
```

**Example — set AC to cool mode at 24°C**:
Two separate nodes needed — one for mode, one for temperature:
```json
{
  "config": {
    "device_uuid": "ac-uuid",
    "action": "set_mode",
    "params": { "mode": "cool" }
  }
}
```
```json
{
  "config": {
    "device_uuid": "ac-uuid",
    "action": "set_target_temperature",
    "params": { "target_temperature": 24 }
  }
}
```

**Example — using template in params** (resolve trigger value at runtime):
```json
{
  "config": {
    "device_uuid": "dimmer-uuid",
    "action": "set_brightness",
    "params": { "brightness": "{{trigger.new_value}}" }
  }
}
```

### notify — Send Notification

Sends a system notification or email.

**Config (`CfgNotify`)**:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `channel` | string | yes | `"system"` or `"email"` |
| `title` | string | no | Notification title (default: "KNX Gateway" for email) |
| `body` | string | yes | Notification body (supports templates like `{{trigger.new_value}}`) |
| `level` | string | no | For system: `"info"`, `"warning"`, `"error"`. Default: `"info"` |
| `smtp_host` | string | for email | SMTP server address (e.g. `"smtp.exmail.qq.com"`) |
| `smtp_port` | int | for email | SMTP port (e.g. `587` for STARTTLS, `465` for SSL) |
| `smtp_user` | string | for email | SMTP login username |
| `smtp_pass` | string | for email | SMTP login password |
| `smtp_ssl` | bool | no | Use implicit TLS/SSL (port 465). Default: false (STARTTLS) |
| `email_to` | string | for email | Recipient email address |

**Example — system notification**:
```json
{
  "config": {
    "channel": "system",
    "body": "Temperature exceeded threshold: {{trigger.new_value}}°C",
    "level": "warning"
  }
}
```

> **Security**: Email notifications transmit notification content to an external SMTP
> server. Store SMTP credentials in environment variables rather than hardcoding them
> in the workflow config.

**Example — email notification**:
```json
{
  "config": {
    "channel": "email",
    "title": "Temperature Alert",
    "body": "Current temperature is {{trigger.new_value}}°C",
    "smtp_host": "smtp.example.com",
    "smtp_port": 587,
    "smtp_user": "[REDACTED_SECRET]",
    "smtp_pass": "SMTP_PASSWORD_PLACEHOLDER",
    "smtp_ssl": false,
    "email_to": "[REDACTED_SECRET]"
  }
}
```

### mqtt_publish — Publish MQTT Message

Publishes a message to an MQTT broker.

> **Security**: If `broker_uri` points outside the trusted LAN, device data is transmitted over
> the network. Use TLS (`ssl://`) for non-local brokers and avoid `tls_skip_verify: true`
> in production.

**Config (`CfgMqttPublish`)**:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `topic` | string | yes | MQTT topic |
| `payload` | string | yes | Message payload (supports templates) |
| `broker_uri` | string | no | Broker URI (default: `tcp://localhost:1883`). Supports `tcp://`, `ssl://`, `tls://` |
| `username` | string | no | MQTT auth username |
| `password` | string | no | MQTT auth password |
| `tls_skip_verify` | bool | no | Skip TLS certificate verification |
| `qos` | int | no | QoS level: 0, 1, or 2. Default: 0 |
| `retain` | bool | no | Retain message. Default: false |

**Example — publish to local broker**:
```json
{
  "config": {
    "topic": "home/living-room/temperature",
    "payload": "{{trigger.new_value}}"
  }
}
```

### http_request — HTTP Request

Makes an HTTP request to a reviewed URL.

> **Security**: This node sends data outside the local network. Verify the target URL and
> any included headers or body content before enabling the workflow. Do not use URLs from
> untrusted workflow sources.

**Config (`CfgHttpRequest`)**:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `method` | string | yes | HTTP method: `GET`, `POST`, `PUT`, `DELETE` |
| `url` | string | yes | Target URL |
| `headers` | object | no | Key-value header pairs |
| `body` | string | no | Request body (supports templates) |
| `timeout_s` | int | no | Request timeout in seconds. Default: 30 |

**Example**:
```json
{
  "config": {
    "method": "POST",
    "url": "http://lan-notifier.local/api/events",
    "headers": { "Content-Type": "application/json" },
    "body": "{\"temperature\": {{trigger.new_value}}}",
    "timeout_s": 10
  }
}
```

### knx_send — Send KNX Telegram

Sends one or more KNX group write/response telegrams.

**Config (`CfgKnxSend`)**:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `targets` | array | yes | Array of KNX send targets |

Each target:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `address` | string | yes | KNX group address (e.g. `"1/2/3"`) |
| `command_type` | string | yes | `"write"` or `"response"` |
| `dpt` | string | yes | KNX DPT (e.g. `"1.001"`, `"5.001"`, `"9.001"`) |
| `value` | ValueRef | yes | Value to send |

**Example — turn on KNX switch**:
```json
{
  "config": {
    "targets": [{
      "address": "1/1/1",
      "command_type": "write",
      "dpt": "1.001",
      "value": { "source": "literal", "value": "1" }
    }]
  }
}
```

**Example — set KNX dimmer to trigger value**:
```json
{
  "config": {
    "targets": [{
      "address": "1/2/1",
      "command_type": "write",
      "dpt": "5.001",
      "value": { "source": "trigger", "value": "new_value" }
    }]
  }
}
```

### scene_exec — Execute a Scene

Executes a scene by UUID. Runs all device actions configured in the scene,
with error tolerance (partial failures do not block remaining actions).

**Config (`CfgSceneExec`)**:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `scene_uuid` | string | yes | UUID of the scene to execute (from `GET /scenes`) |

**Output**: Returns execution result with success/failure counts.

**Output data**:
```json
{
  "scene_uuid": "a1b2c3d4-...",
  "success": true,
  "actions_executed": 5,
  "actions_failed": 0,
  "duration_ms": 320
}
```

**Example**:
```json
{
  "uuid": "n1",
  "node_type": "action",
  "node_subtype": "scene_exec",
  "label": "Run Good Night Scene",
  "config": {
    "scene_uuid": "SCENE_UUID_GOOD_NIGHT"
  },
  "enabled": true,
  "pos_x": 500, "pos_y": 200
}
```

> **scene_exec vs device_control for scenes**: Both can trigger a scene.
> - `scene_exec` takes a `scene_uuid` directly — simpler, purpose-built.
> - `device_control` with a virtual scene device UUID + `action: "activate_scene"` works
>   identically but uses the generic device control path.
> Prefer `scene_exec` in automation workflows for clarity.

### set_variable — Set Workflow Variable

Stores a value in the workflow execution context for use by downstream nodes.

> **Note**: `set_variable` is categorized as an `action` node type but does not control any device.
> The stored value is accessible downstream via the `{{var.<key>}}` template or `{ "source": "variable", "value": "<key>" }` ValueRef.

**Config (`CfgSetVariable`)**:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `key` | string | yes | Variable name |
| `value` | ValueRef | yes | Value to store |

**Example**:
```json
{
  "config": {
    "key": "threshold",
    "value": { "source": "literal", "value": "25" }
  }
}
```

---

## Logic Nodes (`node_type: "logic"`)

Logic nodes combine boolean inputs from upstream condition nodes.
They require at least 2 incoming edges from condition nodes (NOT directly from triggers).

**Output handles**: `true`, `false`

### and — All Inputs True

```json
{
  "uuid": "n1",
  "node_type": "logic",
  "node_subtype": "and",
  "label": "All Conditions",
  "config": {},
  "enabled": true,
  "pos_x": 500, "pos_y": 200
}
```

### or — Any Input True

```json
{
  "uuid": "n1",
  "node_type": "logic",
  "node_subtype": "or",
  "label": "Any Condition",
  "config": {},
  "enabled": true,
  "pos_x": 500, "pos_y": 200
}
```

**Validation rule**: Logic nodes must NOT directly receive edges from triggers. Always
place a condition node between a trigger and a logic node.

---

## Delay Nodes (`node_type: "delay"`)

Delay nodes pause execution for a specified duration.

**Output handle**: `output`

### fixed_delay — Fixed Wait

**Config (`CfgFixedDelay`)**:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `seconds` | int | yes | Delay duration in seconds. Must not exceed `workflow.timeout_s` |

```json
{
  "uuid": "n1",
  "node_type": "delay",
  "node_subtype": "fixed_delay",
  "label": "Wait 10s",
  "config": { "seconds": 10 },
  "enabled": true,
  "pos_x": 400, "pos_y": 200
}
```

**Validation rule**: `seconds` must be ≤ the workflow's `timeout_s`.

---

## Transform Nodes (`node_type: "transform"`)

Transform nodes modify data flowing through the workflow.

**Output handle**: `output`

### value_map — Map Values

Maps input values to output values using a lookup table.

**Config (`CfgValueMap`)**:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `input` | ValueRef | yes | Input value to look up |
| `mapping` | object | yes | Key-value mapping `{ "input": "output" }` |
| `default` | string | no | Default output if no mapping matches |
| `output_key` | string | yes | Name for the output variable |

**Example — map motion to actions**:
```json
{
  "config": {
    "input": { "source": "trigger", "value": "new_value" },
    "mapping": {
      "true": "occupied",
      "false": "vacant"
    },
    "default": "unknown",
    "output_key": "room_status"
  }
}
```

The mapped result is stored as a workflow variable. Access it in downstream nodes:
- Template syntax: `{{var.room_status}}`
- ValueRef: `{ "source": "variable", "value": "room_status" }`

---

## Template Syntax

String values in node configs support Go template syntax for dynamic values:

| Template | Resolves to |
|----------|-------------|
| `{{trigger.new_value}}` | New value from trigger event |
| `{{trigger.old_value}}` | Previous value |
| `{{trigger.device_uuid}}` | Trigger device UUID |
| `{{trigger.device_name}}` | Trigger device name |
| `{{trigger.property}}` | Trigger property name |
| `{{trigger.knx_value}}` | KNX trigger parsed value |
| `{{trigger.knx_address}}` | KNX trigger group address |
| `{{var.key_name}}` | Workflow variable |

Templates work in: `body`, `payload`, `url`, `params` string values, and ValueRef `value` fields.


path: ref/scenes.md
content: # Scene Reference

## What is a Scene?

A scene is a reusable collection of device actions that execute together as a group.
When triggered, the system runs all actions in order (respecting per-action delays).
If an intermediate action fails, remaining actions still execute (error tolerance).

Each scene auto-creates a **virtual device** (`protocol: "scene"`) that allows it to
be controlled through the standard device control API and used in automation workflows.

---

## Scene Data Model

```json
{
  "id": 1,
  "uuid": "a1b2c3d4-e5f6-...",
  "name": "Good Night",
  "description": "Turn off all lights and close curtains",
  "icon": "moon",
  "color": "#6366F1",
  "actions": [ ... ],
  "knx_bind_address": "",
  "knx_bind_dpt": "",
  "knx_scene_id": null,
  "device_uuid": "scene-a1b2c3d4-e5f6-...",
  "enabled": true,
  "sort_order": 0,
  "exec_total": 42,
  "exec_success": 40,
  "exec_fail": 2,
  "last_exec_at": "2026-04-07T10:30:00Z",
  "created_at": "2026-03-01T08:00:00Z",
  "updated_at": "2026-04-07T10:30:00Z"
}
```

### Core Fields

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `name` | string | **yes** | — | Scene name (max 100 chars) |
| `description` | string | no | `""` | Description (max 500 chars) |
| `icon` | string | no | `"home"` | Icon identifier (Material/Lucide name, e.g. `"moon"`, `"sun"`, `"play"`) |
| `color` | string | no | `"#3B82F6"` | Hex color for UI display |
| `actions` | array | no | `[]` | Device actions to execute (see below) |
| `enabled` | bool | no | `true` | Whether the scene can be executed |

### KNX Binding Fields (optional)

These fields bind a scene to a physical KNX scene controller so that
pressing a button on the KNX panel triggers the software scene.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `knx_bind_address` | string | no | KNX group address to listen on (e.g. `"1/2/3"`) |
| `knx_bind_dpt` | string | no | KNX DPT for value parsing. Use `"18.001"` (Scene Control) for KNX scene panels |
| `knx_scene_id` | int\|null | no | Scene number (0–63) from the KNX panel. `null` = not bound |

**DPT 18.001 (Scene Control)**: This is a call-only KNX datapoint — the gateway
receives scene activation commands from KNX panels but does not learn/store scenes
on the KNX side. When a KNX panel sends scene number N to the bound address,
the gateway matches `knx_scene_id == N` and executes the scene's actions.

**Example — bind scene to KNX wall panel**:
```json
{
  "name": "Living Room Evening",
  "knx_bind_address": "1/7/0",
  "knx_bind_dpt": "18.001",
  "knx_scene_id": 3,
  "actions": [ ... ]
}
```
When the KNX panel sends scene 3 to group address `1/7/0`, this scene executes.

### Auto-Generated Fields (read-only)

| Field | Description |
|-------|-------------|
| `uuid` | Server-generated UUID for the scene |
| `device_uuid` | UUID of the auto-created virtual device (`scene-<uuid>`) |
| `sort_order` | Display order in the UI |
| `exec_total` | Total execution count |
| `exec_success` | Successful execution count |
| `exec_fail` | Failed execution count |
| `last_exec_at` | Timestamp of last execution |

---

## Actions Array

The `actions` field is a JSON array of device actions. Each action represents one
command to send to a device.

### Action Structure

```json
{
  "id": "a1",
  "device_uuid": "light-living-room-uuid",
  "device_name": "Living Room Light",
  "action": "turn_off",
  "params": {},
  "delay": 0
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | **yes** | Unique action ID within the scene (e.g. `"a1"`, `"a2"`) |
| `device_uuid` | string | **yes** | UUID of the target device (from `GET /devices`) |
| `device_name` | string | **yes** | Display name of the device (for UI rendering) |
| `action` | string | **yes** | Device action name — must match device capability (see [devices.md](devices.md)) |
| `params` | object | **yes** | Action parameters — must match action schema (see [devices.md](devices.md)) |
| `delay` | int | no | Delay in milliseconds before executing this action. `0` = immediate |

### Action/Params Reference by Device Type

Actions and params must exactly match what the device supports. Always cross-reference
with [devices.md](devices.md). Common patterns:

| Device Type | Action | Params | Example |
|-------------|--------|--------|---------|
| `light` | `turn_on` | `{}` | Turn on a light |
| `light` | `turn_off` | `{}` | Turn off a light |
| `light_dimmer` | `set_brightness` | `{ "brightness": 50 }` | Set to 50% (auto powers light on) |
| `light_tunable` | `set_color_temperature` | `{ "color_temperature": 3000 }` | Set warm white (range 1000–10000 K, see [devices.md](devices.md)) |
| `light_rgb` | `set_color` | `{ "color": "#FF8800" }` | Set hex color (NOT `hue`/`saturation`) |
| `curtain` | `close` | `{}` | Close curtain |
| `curtain` | `set_position` | `{ "position": 50 }` | Half open |
| `blind` | `set_slat_position` | `{ "slat_position": 45 }` | Tilt slats |
| `ac` | `set_mode` | `{ "mode": "cool" }` | Cooling mode |
| `ac` | `set_target_temperature` | `{ "target_temperature": 24 }` | Set to 24°C |
| `ac` | `set_fan_speed_mode` | `{ "fan_speed_mode": "high" }` | AC default enum: `auto/low/medium/high` |
| `fan` | `set_fan_speed` | `{ "fan_speed": 70 }` | 70% speed |
| `fan` | `set_fan_speed_mode` | `{ "fan_speed_mode": "level2" }` | Fan default enum: `level1/level2/level3` (NOT `low/high` — see [devices.md](devices.md#fan_speed--fan-speed-control)) |
| `switch` | `turn_off` | `{}` | Turn off outlet |
| `lock` | `lock` | `{}` | Lock door |

### Execution Behavior

1. Actions execute **sequentially** in array order
2. Each action's `delay` is waited **before** that action executes
3. **Error tolerance**: If an action fails (device offline, invalid params), execution
   continues with the next action. The final result reports both `actions_executed` and
   `actions_failed`
4. Disabled scenes (`enabled: false`) cannot be executed — the API returns an error

### Example — Complete "Good Night" Scene

```json
{
  "name": "Good Night",
  "description": "Prepare house for sleep",
  "icon": "moon",
  "color": "#6366F1",
  "actions": [
    {
      "id": "a1",
      "device_uuid": "light-living-uuid",
      "device_name": "Living Room Light",
      "action": "turn_off",
      "params": {},
      "delay": 0
    },
    {
      "id": "a2",
      "device_uuid": "light-bedroom-uuid",
      "device_name": "Bedroom Light",
      "action": "set_brightness",
      "params": { "brightness": 10 },
      "delay": 0
    },
    {
      "id": "a3",
      "device_uuid": "curtain-living-uuid",
      "device_name": "Living Room Curtain",
      "action": "close",
      "params": {},
      "delay": 500
    },
    {
      "id": "a4",
      "device_uuid": "ac-bedroom-uuid",
      "device_name": "Bedroom AC",
      "action": "set_target_temperature",
      "params": { "target_temperature": 26 },
      "delay": 0
    },
    {
      "id": "a5",
      "device_uuid": "ac-bedroom-uuid",
      "device_name": "Bedroom AC",
      "action": "set_mode",
      "params": { "mode": "fan_only" },
      "delay": 0
    },
    {
      "id": "a6",
      "device_uuid": "lock-front-uuid",
      "device_name": "Front Door Lock",
      "action": "lock",
      "params": {},
      "delay": 1000
    }
  ],
  "enabled": true
}
```

---

## Execution Result

When a scene is executed (via `POST /scenes/:uuid/execute`, automation `scene_exec`
node, or device control `activate_scene`), the response includes:

```json
{
  "success": true,
  "actions_executed": 6,
  "actions_failed": 0,
  "duration_ms": 1650,
  "errors": []
}
```

| Field | Type | Description |
|-------|------|-------------|
| `success` | bool | `true` if all actions succeeded, `false` if any failed |
| `actions_executed` | int | Number of actions that completed successfully |
| `actions_failed` | int | Number of actions that failed |
| `duration_ms` | int | Total execution time in milliseconds (including delays) |
| `errors` | array | Error messages from failed actions |

---

## Execution Logs

Each execution is recorded in the execution log. Query via:

```
GET /scenes/logs?scene_uuid=xxx&limit=50&offset=0
```

Log entry format:
```json
{
  "id": 1,
  "scene_uuid": "a1b2c3d4-...",
  "scene_name": "Good Night",
  "source": "manual",
  "success": true,
  "duration_ms": 1650,
  "actions_executed": 6,
  "actions_failed": 0,
  "error_message": "",
  "created_at": "2026-04-08T23:00:00Z"
}
```

| Source | Description |
|--------|-------------|
| `manual` | Triggered from the web UI or `POST /scenes/:uuid/execute` |
| `knx` | Triggered by a KNX panel via the bound address |
| `automation` | Triggered from an automation workflow (`scene_exec` node) |

---

## Virtual Scene Device

Each scene automatically creates a virtual device that appears in `GET /devices`:

| Property | Value |
|----------|-------|
| `uuid` | `scene-<scene_uuid>` |
| `type` | `scene` |
| `sub_type` | `scene` |
| `protocol` | `scene` |
| `instance_name` | `scene` |
| `is_online` | always `true` |
| `capabilities` | `["scene"]` |

Virtual scene devices support one action:

| Action | Description |
|--------|-------------|
| `activate_scene` | Execute the scene (same as `POST /scenes/:uuid/execute`) |

Scene enable/disable is managed via the dedicated API (`POST /scenes/:uuid/toggle`), not through device control.

This allows scenes to be triggered through the standard device control API:
```
POST /devices/:id/control
{ "action": "activate_scene", "params": {} }
```

And used in automation workflows via `device_control` nodes (though `scene_exec` is preferred).


path: ref/triggers.md
content: # Trigger Reference

Triggers are the entry points of a workflow. Each workflow can have 1–5 triggers.
Triggers live in `flow_data.triggers[]`.

## Common Trigger Fields

Every trigger has these fields:

```json
{
  "uuid": "placeholder-remapped-on-import",
  "trigger_type": "<type>",
  "label": "Human-readable name",
  "enabled": true,
  "pos_x": 100,
  "pos_y": 200
}
```

## Compare Operators

Used by `device_event` and `knx_group_event` triggers:

| Operator | Meaning | Works on |
|----------|---------|----------|
| `changed` | Value changed (any change fires) | Any |
| `eq` | Equal | Numbers, strings |
| `ne` | Not equal | Numbers, strings |
| `gt` | Greater than | Numbers |
| `lt` | Less than | Numbers |
| `gte` | Greater than or equal | Numbers |
| `lte` | Less than or equal | Numbers |
| `is_true` | Truthy check (not 0, not false, not empty) | Booleans, numbers |
| `is_false` | Falsy check (0, false, empty, null) | Booleans, numbers |

**Important**: For boolean device properties (like `power`, `motion`, `contact`), the
value is normalized before comparison. `true`/`1`/`"true"` → `1`, `false`/`0`/`"false"` → `0`.
So `gte` with value `"1"` and `is_true` both correctly match "on" / "true" states.

---

## 1. manual — Manual Trigger

Fires when a user clicks "Execute" in the UI or calls the execute API.

**Extra fields**: none

```json
{
  "uuid": "t1",
  "trigger_type": "manual",
  "label": "Manual Run",
  "enabled": true,
  "pos_x": 100, "pos_y": 200
}
```

---

## 2. device_event — Device State Change

Fires when a device property changes and the compare condition is met.

**Extra fields**:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `device_uuid` | string | yes | UUID of the device to monitor |
| `device_property` | string | yes | Property name to watch (e.g. `power`, `temperature`, `brightness`) |
| `compare_op` | string | yes | Compare operator (see table above) |
| `compare_value` | string | for most ops | Compare threshold. Not needed for `changed`, `is_true`, `is_false` |

**Trigger data available at runtime**:
- `trigger.device_uuid` — the device UUID
- `trigger.property` — the property name
- `trigger.old_value` — previous value
- `trigger.new_value` — new value
- `trigger.device_status` — full device status map

```json
{
  "uuid": "t1",
  "trigger_type": "device_event",
  "label": "Temperature Above 25",
  "enabled": true,
  "device_uuid": "sensor-uuid-here",
  "device_property": "temperature",
  "compare_op": "gte",
  "compare_value": "25",
  "pos_x": 100, "pos_y": 200
}
```

### Common device_event patterns

**Light turned on**:
```json
{
  "device_uuid": "light-uuid",
  "device_property": "power",
  "compare_op": "is_true",
  "compare_value": ""
}
```

**Motion detected**:
```json
{
  "device_uuid": "motion-sensor-uuid",
  "device_property": "motion",
  "compare_op": "is_true",
  "compare_value": ""
}
```

**Temperature dropped below 18°C**:
```json
{
  "device_uuid": "temp-sensor-uuid",
  "device_property": "temperature",
  "compare_op": "lt",
  "compare_value": "18"
}
```

**Any brightness change**:
```json
{
  "device_uuid": "dimmer-uuid",
  "device_property": "brightness",
  "compare_op": "changed",
  "compare_value": ""
}
```

---

## 3. cron — Scheduled Trigger

Fires on a cron schedule. Supports standard 5-field cron (minute, hour, day, month, weekday)
and 6-field cron with seconds.

**Extra fields**:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `cron_expr` | string | yes | Cron expression |

```json
{
  "uuid": "t1",
  "trigger_type": "cron",
  "label": "Every Day 8AM",
  "enabled": true,
  "cron_expr": "0 8 * * *",
  "pos_x": 100, "pos_y": 200
}
```

### Cron expression examples

| Expression | Meaning |
|-----------|---------|
| `* * * * *` | Every minute |
| `0 8 * * *` | Daily at 8:00 AM |
| `0 8 * * 1-5` | Weekdays at 8:00 AM |
| `0 0 1 * *` | First day of month at midnight |
| `*/5 * * * *` | Every 5 minutes |
| `0 18 * * 0,6` | Weekends at 6:00 PM |
| `0 0 8 * * *` | 6-field: Daily at 8:00:00 AM (with seconds) |

---

## 4. knx_group_event — KNX Group Telegram

Fires when a KNX group write/read/response matches the configured addresses.

**Extra fields**:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `knx_addresses` | string[] | yes | List of KNX group addresses to monitor (e.g. `["1/2/3"]`) |
| `knx_event_types` | string[] | no | Event types to filter: `"write"`, `"read"`, `"response"`. Empty = all |
| `knx_dpt` | string | no | KNX DPT for value parsing (e.g. `"1.001"`, `"9.001"`, `"5.001"`) |
| `compare_op` | string | no | Compare operator for parsed value |
| `compare_value` | string | no | Compare threshold |

**Trigger data available at runtime**:
- `trigger.knx_address` — the matching group address
- `trigger.knx_command` — the command type (write/read/response)
- `trigger.knx_dpt` — the DPT used for parsing
- `trigger.knx_value` — the parsed value
- `trigger.knx_source` — the source KNX address

```json
{
  "uuid": "t1",
  "trigger_type": "knx_group_event",
  "label": "KNX Switch Input",
  "enabled": true,
  "knx_addresses": ["1/2/3"],
  "knx_event_types": ["write"],
  "knx_dpt": "1.001",
  "compare_op": "is_true",
  "compare_value": "",
  "pos_x": 100, "pos_y": 200
}
```

### Common KNX DPT types

| DPT | Type | Description | Value range |
|-----|------|-------------|-------------|
| `1.001` | Boolean (Switch) | On/Off | 0 or 1 |
| `1.002` | Boolean | True/False | 0 or 1 |
| `1.008` | Boolean (Up/Down) | Up/Down | 0 or 1 |
| `5.001` | 8-bit unsigned (Percentage) | 0–100% | 0–100 |
| `5.010` | 8-bit unsigned (Counter) | Pulse counter | 0–255 |
| `9.001` | 2-byte float (Temperature) | Temperature °C | -273 to 670760 |
| `9.004` | 2-byte float (Illuminance) | Lux | 0–670760 |
| `9.007` | 2-byte float (Humidity) | Humidity % | 0–670760 |
| `14.056` | 4-byte float (Power) | Watts | float |

---

## 5. sun_event — Sunrise/Sunset

Fires at sunrise or sunset, calculated from the system's geographic location
(latitude/longitude). Supports an optional minute offset for triggering before
or after the actual event.


**Scheduling behavior**:
- **Zero offset** (`sun_offset_min: 0` or omitted): triggered by the system
  monitor's sunrise/sunset event bus (accurate to ~30 seconds).
- **Non-zero offset**: uses per-trigger timer scheduling — computes the exact
  next fire time daily based on astronomical data, re-schedules automatically
  after each firing and when the system location changes.

**Extra fields**:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `sun_event` | string | yes | `"sunrise"` or `"sunset"` |
| `sun_offset_min` | int | no | Offset in minutes (negative = before, positive = after). Range: -720 to 720. Default: 0 |

**Trigger data available at runtime**:
- `trigger.new_value` — `"sunrise"` or `"sunset"`
- `trigger.source` — `"system"`

```json
{
  "uuid": "t1",
  "trigger_type": "sun_event",
  "label": "30 min before sunset",
  "enabled": true,
  "sun_event": "sunset",
  "sun_offset_min": -30,
  "pos_x": 100, "pos_y": 200
}
```

**Example — at sunrise (no offset)**:
```json
{
  "uuid": "t1",
  "trigger_type": "sun_event",
  "label": "Sunrise",
  "enabled": true,
  "sun_event": "sunrise",
  "pos_x": 100, "pos_y": 200
}
```

---

## 6. webhook — Inbound HTTP Trigger

Fires when the workflow's webhook URL is called by an approved client.

Use webhook triggers only when the user explicitly wants inbound HTTP access to
the gateway workflow. Keep the token in the gateway configuration or secret store;
do not place a real token in documentation or shared workflow examples.

**Extra fields**:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `webhook_token` | string | yes | Secret token for the webhook URL |

```json
{
  "uuid": "t1",
  "trigger_type": "webhook",
  "label": "Inbound Trigger",
  "enabled": true,
  "webhook_token": "WEBHOOK_TOKEN_PLACEHOLDER",
  "pos_x": 100, "pos_y": 200
}
```

**Trigger data available at runtime**:
- `trigger.webhook_payload` — the JSON body sent to the webhook


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)