# Controller Profiles

This directory contains **controller profiles** — the translation layer between
the package's nodes and specific hardware/firmware payload formats. Profiles are
organised by node type:

```
profiles/
├── dmx/      → ha-mqtt-dmx        (output: DMX channel values)
├── relay/    → ha-mqtt-relay      (output: relay switching)
├── button/   → ha-mqtt-button     (input: wall button events)
├── pir/      → ha-mqtt-pir        (input: motion sensor events)
├── target/   → target-system adapters (Home Assistant / standalone) —
│               a DIFFERENT contract, not covered by this document
└── index.js  → the registry (organised by node type)
```

---

## Built-in profiles

### DMX (`profiles/dmx/`)

| Profile ID | Dropdown label | Payload |
|---|---|---|
| `pragiom_dmx_v1` | EtherTen v1 — Legacy | `"212255"` (3-digit channel + 3-digit value, one msg per channel) |
| `pragiom_dmx_v2` | EtherTen v2 — Batched | `[[212,255],...]` (one msg per DMX tick) |
| `mqtt_json` | MQTT JSON | `{"channel":212,"value":255}` |
| `custom` | Custom payload | Quarantined — not selectable in the editor (2026-07-07); wiring is roadmap D1 |

### Relay (`profiles/relay/`)

| Profile ID | Dropdown label | Payload |
|---|---|---|
| `pragiom_relay_v1` | Relay Controller v1 | `"1"` / `"0"` |
| `generic_onoff` | Generic ON/OFF | `"ON"` / `"OFF"` (Tasmota/Shelly style) |
| `mqtt_json` | MQTT JSON | `{"state":true}` |
| `custom` | Custom | Quarantined — see note above |

### Button (`profiles/button/`)

| Profile ID | Dropdown label | Format |
|---|---|---|
| `pragiom_btn_v1` | Controller v1 — Legacy | `panelId-GPIO` string match |
| `pragiom_btn_v2` | Controller v2 — JSON | `{"id":"S-1-A","gpio":62,"state":"on"}`, matched by fixture ID |
| `custom` | Custom | Quarantined — see note above |

### PIR (`profiles/pir/`)

| Profile ID | Dropdown label | Format |
|---|---|---|
| `pragiom_pir_v1` | Controller v1 — Legacy | `panelId-GPIO` string match (motion only) |
| `pragiom_pir_v2` | Controller v2 — JSON | Fixture-ID JSON with motion + clear events |
| `custom` | Custom | Quarantined — see note above |

---

## Profile contracts

The contract differs between **output** profiles (dmx, relay — the node builds
and publishes payloads) and **input** profiles (button, pir — the node parses
inbound payloads).

### DMX output profile

```javascript
module.exports = {
  id:      'my_dmx_controller',        // unique, stored in flow JSON
  label:   'My Controller (Brand)',    // shown in the editor dropdown
  batched: false,                      // true = one message per DMX tick

  // Called as buildPayload(channel, value, batch):
  //   per-channel profiles receive (channel, value, null)
  //   batched profiles receive     (null, null, [[ch,val], ...])
  // Return the string/object to publish.
  buildPayload: (channel, value, batch) => {
    return `${channel}=${value}`;
  },

  // cfg is the resolved Zone config node (cfg.siteId, cfg.zone,
  // cfg.buildTopic(...), etc). universe is the node's DMX universe.
  buildTopic: (cfg, universe) => {
    return `${cfg.siteId}/${cfg.zone}/dmx/${universe}`;
  },

  configFields: []   // reserved for future editor-rendered fields
};
```

### Relay output profile

```javascript
module.exports = {
  id:    'my_relay_controller',
  label: 'My Relay (Brand)',

  buildPayload: (state) => state ? 'ON' : 'OFF',   // state is boolean

  buildTopic: (cfg, controllerNum, relayNum) =>
    `${cfg.siteId}/${cfg.zone}/${controllerNum}/relay/${relayNum}/command`,

  configFields: []
};
```

### Button / PIR input profile

```javascript
module.exports = {
  id:      'my_button_controller',
  label:   'My Buttons (Brand)',
  version: 2,

  // Topic the node subscribes to for this hardware family.
  buildTopic: (cfg) => `${cfg.siteId}/${cfg.zone}/sensors/buttons`,

  // Called for every inbound message on the subscribed topic.
  // MUST return { match: true, state: 'on'|'off', ... } when the
  // message belongs to this fixture, and null otherwise — a result
  // without `match: true` is treated as "not mine".
  parsePayload: (rawPayload, fixtureId, expectedPayload) => {
    try {
      const data = JSON.parse(rawPayload);
      if (data.id === fixtureId) {
        return { match: true, state: data.state || 'on' };
      }
    } catch (e) {}
    return null;
  },

  // Hide editor fields this profile auto-derives:
  showPayloadField: false,
  showTopicField:   false,

  configFields: []
};
```

### Virtual / software-only outputs

The DMX and relay channel-conflict checks assume one channel/relay number =
one physical output. If your profile represents a purely virtual output where
a "conflict" is meaningless, declare `hasPhysicalAddressing: false` on the
profile object — see the convention note in `index.js` for the full contract.

---

## Registering a profile

Two steps — both are required:

1. **The registry.** Add your `require()` line to the matching node-type array
   in `profiles/index.js`:

```javascript
module.exports = {
  dmx: [
    require('./dmx/pragiom_dmx_v1'),
    require('./dmx/pragiom_dmx_v2'),
    require('./dmx/mqtt_json'),
    require('./dmx/custom'),
    require('./dmx/my_dmx_controller'),   // ← add here
  ],
  // relay: [...], button: [...], pir: [...]
};
```

2. **The editor dropdown.** The Controller Profile dropdowns in the node
   editors are static HTML — add a matching `<option>` to the relevant
   `nodes/ha-mqtt-*.html` file:

```html
<option value="my_dmx_controller">My Controller (Brand)</option>
```

Skipping step 2 means your profile works in flow JSON but is impossible to
select in the editor.

---

## Submitting a profile

1. Fork the repository
2. Create your profile file and register it (both steps above)
3. Describe in the pull request how you tested it — which hardware/firmware,
   and what you verified end-to-end (discovery, command, state)
4. Submit the pull request

Your profile will be reviewed and merged if it implements the full contract
for its node type, has clear comments, and doesn't break existing profiles.

---

## Licence

All profiles in this directory are licensed under GPL v3.
See the root LICENSE file for details.

By submitting a profile you agree to license it under GPL v3.
