# Configuration

`node-red-contrib-lsh-logic` has two configuration layers:

- the Node-RED editor settings, which describe MQTT paths, timing, and context
  exports;
- the inline **System Config** JSON, which describes your LSH devices and the
  long-click actions that this node should orchestrate.

The mental model: the editor config tells the node **where to listen and how to
behave**; the inline JSON tells it **which devices exist and what user actions
mean**.

## Node-RED Editor Settings

| Field                       | Meaning                                                                              |
| --------------------------- | ------------------------------------------------------------------------------------ |
| `homieBasePath`             | Homie lifecycle base path, for example `homie/5/`.                                   |
| `lshBasePath`               | LSH device topic base path, for example `LSH/`.                                      |
| `serviceTopic`              | Bridge-scoped service topic for broadcast `PING` and startup `BOOT` replay requests. |
| `protocol`                  | LSH payload encoding: `json` or `msgpack`.                                           |
| `systemConfigJson`          | Inline JSON device/action configuration stored in the Node-RED flow.                 |
| `clickTimeout`              | Hard request/ACK/confirm timeout for distributed click actions.                      |
| `clickCleanupInterval`      | Periodic sweep for expired click transactions.                                       |
| `initialStateTimeout`       | Startup replay window used only when bridge-local `BOOT` replay is actually needed.  |
| `watchdogInterval`          | Periodic health-check interval.                                                      |
| `interrogateThreshold`      | Silence threshold before the watchdog sends a probe.                                 |
| `pingTimeout`               | Time to wait for a ping reply before a device is considered stale.                   |
| `exposeStateContext` / key  | Optional live registry export to flow/global context.                                |
| `exportTopics` / key        | Optional generated MQTT topic export to flow/global context.                         |
| `exposeConfigContext` / key | Optional effective runtime config export to flow/global context.                     |
| `otherActorsContext`        | Context store used to read external actor states.                                    |
| `otherDevicesPrefix`        | Prefix used for external actor state lookups.                                        |

MQTT base paths must end with `/`, contain no empty segments and contain no MQTT
wildcards. Publish topics such as `serviceTopic` must be concrete topics and
must not end with `/`.

## Recommended Defaults

For a typical LSH v5 setup:

| Setting                   | Typical value                                      |
| ------------------------- | -------------------------------------------------- |
| Homie Base Path           | `homie/5/`                                         |
| LSH Base Path             | `LSH/`                                             |
| Service Topic             | `LSH/Node-RED/SRV`                                 |
| LSH Protocol              | `JSON` unless firmware uses MsgPack                |
| System Config             | inline JSON in the node editor                     |
| Export MQTT Topics        | `flow`, key `lsh_topics`                           |
| Export Internal State     | `none`, unless dashboards or sync helpers need it  |
| Export Effective Config   | `none`, unless helper nodes need exported settings |
| Read External Actor State | `flow` for same-flow state, `global` across flows  |

The default timing values are intentionally conservative. Tune them only when
you understand your bridge latency, controller timeout, and broker behavior.

## System Config JSON

This JSON is only for LSH orchestration. Keep Home Assistant discovery mapping
in the optional Homie discovery node:
`node-red-contrib-homie-home-assistant-discovery`. That is where Home Assistant
entity names, icons, platforms, and discovery IDs belong.

Edit it directly in the node dialog. That makes exported flows self-contained:
the runtime config travels with the flow instead of depending on a separate file
inside the Node-RED user directory.

The editor uses the LSH System Config schema to validate the JSON, rejects
unknown properties, checks that LSH actor targets reference configured devices,
and previews the exact MQTT subscriptions generated from the current paths and
device list. It can also import the `lsh-stack-config/v1` JSON emitted by
`lsh-core`; that fills the MQTT paths, payload protocol, System Config JSON and
preview QoS values from the generated stack export.

## Minimal Example

JSON files cannot contain comments. The first block is `jsonc` to explain the
file; the second block is valid JSON that you can copy.

```jsonc
{
  // Every LSH device known to the orchestration layer.
  "devices": [
    {
      // Device ID. It must match MQTT topics exactly, for example LSH/c1/state.
      "name": "c1",

      // Optional actions fired by long-click events from this device.
      "longClickButtons": [
        {
          // Button ID reported by the controller event.
          "id": 1,

          // LSH devices controlled by this long-click.
          "actors": [
            {
              // Target device. It must exist in devices[].
              "name": "j1",

              // true means the whole target device is affected.
              "allActuators": true,

              // Must be empty when allActuators is true.
              "actuators": [],
            },
          ],

          // Optional non-LSH targets emitted on output 2.
          "otherActors": ["zigbee_table_lamp"],
        },
      ],
    },

    // A device can be listed only to make it known and monitored.
    {
      "name": "j1",
    },
  ],
}
```

Copyable JSON:

```json
{
  "devices": [
    {
      "name": "c1",
      "longClickButtons": [
        {
          "id": 1,
          "actors": [
            {
              "name": "j1",
              "allActuators": true,
              "actuators": []
            }
          ],
          "otherActors": ["zigbee_table_lamp"]
        }
      ]
    },
    {
      "name": "j1"
    }
  ]
}
```

## Device Entries

`devices[].name` must match the exact MQTT device ID and must be a single topic
segment using letters, digits, `_` or `-`. Names are checked case-insensitively,
so `C1` and `c1` cannot coexist.

A device entry can be just:

```json
{ "name": "j1" }
```

That is enough for the node to monitor the device, subscribe to its topics and
target it from another device's click action.

## Long Clicks and Super Long Clicks

Two action lists are available:

- `longClickButtons`
- `superLongClickButtons`

They have the same shape. The difference is the controller event that triggers
them.

Each action needs:

- `id`: the numeric button ID reported by the device;
- at least one target between `actors` and `otherActors`.

Example with a partial LSH target and one external actor:

```json
{
  "id": 2,
  "actors": [
    {
      "name": "kitchen-board",
      "allActuators": false,
      "actuators": [1, 2, 3]
    }
  ],
  "otherActors": ["zigbee_table_lamp"]
}
```

## LSH Actors

`actors` target devices controlled by this node.

Rules:

- `name` must match a configured `devices[].name` exactly.
- `allActuators: true` targets the whole device and must use `actuators: []`.
- `allActuators: false` targets only listed actuator IDs and requires a
  non-empty `actuators` array.

Before a distributed click is confirmed, the node checks that each targeted LSH
device has an authoritative actuator snapshot. If a target is reachable but
state is missing, the click fails cleanly instead of choosing a toggle direction
from incomplete information.

## Other Actors

`otherActors` are names for non-LSH targets. The node does not know whether they
are Zigbee lights, Tasmota plugs, Home Assistant services, or something else.

Instead, it emits a generic command on output 2. Your surrounding Node-RED flow
reads that message and translates it to the right protocol.

This keeps the LSH runtime focused on LSH correctness while still letting the
same button action reach both LSH and external targets.

For toggle semantics, the coordinator only trusts boolean state stored in the
configured external actor context. Actor `zigbee_table_lamp` with prefix
`other_devices` is read from:

```text
other_devices.zigbee_table_lamp.state
```

Use `lsh-external-state` to keep those values current from MQTT or other
Node-RED integrations.

## Applying Changes

Configuration changes become active when you deploy the Node-RED flow. The flow
JSON is the single source of truth, and deployment is the point where Node-RED
makes runtime changes effective.

When a new inline config is valid:

- the coordinator starts from the new config;
- pending click transactions are cleared;
- MQTT subscriptions are updated only if the effective topic set changed.

When the inline JSON is invalid, deployment stops with a clear configuration
error. Fix the JSON in the editor and deploy again.

## Context Exports

Context exports are optional. Enable them when you want dashboards, debug flows,
or observability.

- Internal state export gives you a detached snapshot of the live device
  registry.
- Topic export gives you the MQTT subscription set generated from config:
  grouped `lsh`, `homie`, and `all` topic arrays, plus a `subscriptions` map
  keyed by topic with QoS values.
- Effective config export gives you the normalized Node-RED node configuration
  as a flat object, plus `systemConfig` with the parsed inline JSON and
  `lastUpdated`.

For normal production flows, it is fine to leave internal state and effective
config exports disabled and keep only topic export enabled if you use dynamic
subscriptions.

### Context Scope Choices

The editor uses three context choices for optional exports:

- `none` disables an optional export. Use it when no dashboard, debug flow or
  helper node reads that data. This reduces context writes and avoids accidental
  key collisions. `none` is not available for **Read External Actor State**
  because that setting is a read location, not an export; if your System Config
  has no `otherActors`, the selected read context is simply unused.
- `flow` stores or reads data in the current Node-RED flow tab. Use it when the
  `lsh-logic` node, its helper nodes and its MQTT wiring live together. It is
  the safest scope for importable examples and self-contained installations
  because another flow cannot accidentally read or overwrite the same keys.
- `global` stores or reads data across flow tabs. Use it when external state is
  collected in one flow and consumed by a coordinator in another, when dashboards
  need cross-flow access, or when shared helper flows must read the same
  coordinator exports. With `global`, choose explicit keys and prefixes because
  every flow can see the same namespace.

For `lsh-actuator-sync`, enable both **Export Internal State** and **Export
Effective Config** on the owning `lsh-logic` node, then point the helper at the
same context/key pair. For `lsh-external-state`, use the same context selected
by `lsh-logic` in **Read External Actor State**.

### Multiple LSH Logic Nodes

You can run multiple `lsh-logic` nodes in the same Node-RED runtime. This is
useful when you intentionally split responsibilities:

- separate physical LSH installations or MQTT topic spaces;
- production and test coordinators on the same Node-RED host;
- independent floors, buildings or controller groups;
- gradual migrations where one coordinator owns only a subset of devices.

Each `lsh-logic` instance has its own in-memory coordinator. They do not share
hidden runtime state, but they can still conflict through MQTT topics and
Node-RED context keys. Keep these boundaries explicit:

- avoid letting two coordinators command the same `homieBasePath` device and
  actuator unless the duplication is deliberate;
- use distinct `lshBasePath`, `homieBasePath` and `serviceTopic` values for
  truly separate LSH stacks;
- keep each `systemConfigJson` responsible for a clear, non-overlapping device
  set;
- use unique `exposeStateKey`, `exposeConfigKey` and `exportTopicsKey` values
  whenever those exports are enabled;
- use different `otherDevicesPrefix` values when two coordinators read external
  actors with overlapping names;
- point every helper node at the state/config exports of the coordinator it is
  meant to support.

Sharing `global` context is fine when it is intentional, for example when many
flows publish external actor state for one coordinator. It becomes risky when
several coordinators use the same default keys, because a helper may read the
latest export from the wrong coordinator. Prefer `flow` for isolated flows and
switch to `global` only when cross-flow sharing is part of the design.

## External State Helper

`lsh-external-state` stores state for non-LSH actors used in `otherActors`. It
does not subscribe to MQTT by itself. Keep each integration in normal Node-RED
nodes, then feed its state messages into this helper.

Recommended wiring:

1. In `lsh-logic`, set **Read External Actor State** to `flow` or `global`.
2. If you use **Prefix: From lsh_config export**, also enable **Export Effective
   Config** and keep the default key `lsh_config`, or point the helper at your
   custom key.
3. Feed external state messages into `lsh-external-state`.
4. Configure **Actor Name** as a fixed value for one-device nodes, or read it
   from a message property such as `msg.topic` after a Change node.
5. Configure **State Property** for the integration payload.

The helper writes:

```text
<otherDevicesPrefix>.<actorName>.state = true|false
```

When **Metadata** is enabled, it also writes diagnostic fields beside the state:

| Field         | Meaning                                       |
| ------------- | --------------------------------------------- |
| `updatedAt`   | Node-RED timestamp of the accepted update.    |
| `sourceTopic` | Original `msg.topic`, when present.           |
| `rawState`    | Raw value read from the configured property.  |
| `retain`      | Whether the input message had `retain: true`. |

The coordinator ignores metadata and reads only `.state`.

Common state mappings:

| Integration         | Actor Name            | State Property  |
| ------------------- | --------------------- | --------------- |
| ESPHome light MQTT  | Fixed, or `msg.topic` | `payload.state` |
| Zigbee2MQTT switch  | Fixed, or `msg.topic` | `payload.state` |
| Shelly status topic | Fixed, or `msg.topic` | `payload.ison`  |
| Already normalized  | Fixed, or `msg.topic` | `payload`       |

Boolean and numeric `1/0` values are accepted directly. Text values are matched
against the configured true/false lists. Defaults cover `on/off`, `true/false`,
`yes/no`, `1/0`, `open/closed` and `active/inactive`. Missing, invalid or
ambiguous values are dropped with a warning; they are never silently converted
to `false`.

Retained MQTT messages are accepted by default because this helper only stores
observed state and does not command devices. That makes startup recovery work
when external devices publish retained state. Set **Retained Messages** to
**Ignore retained states** only for integrations whose retained state is known
to be stale or misleading.

When the prefix is read from `lsh_config` and the config export is not ready
yet, the helper keeps the latest update per actor and retries until **Context
Ready Wait** expires. The default wait is `5000` ms. Set it to `0` for
fail-fast behavior.

### Multiple External State Helpers

You can use multiple `lsh-external-state` nodes in the same Node-RED runtime.
They do not share hidden runtime state; each accepted message writes only the
configured context path:

```text
<storeContext>:<otherDevicesPrefix>.<actorName>.state
```

That makes multiple instances safe when each one writes a different actor name,
or when separate flows intentionally use different context stores or prefixes.
Use multiple helpers when it makes the flow clearer:

- one helper per integration family, such as ESPHome, Zigbee2MQTT, Shelly or
  Home Assistant;
- one helper per room or functional area;
- separate helpers for payloads that need different **State Property**,
  true/false lists, retained-message policy or metadata policy;
- fixed actor names for simple single-device flows, and message-derived actor
  names for shared fan-in flows.

Avoid wiring two helpers to write the same actor under the same context and
prefix unless that is deliberate. In that case the last accepted update wins,
which is useful for redundant state sources but confusing when two integrations
can report different truth values for the same actor.

## Actuator Sync Helper

`lsh-actuator-sync` is a separate helper node for a specific edge case: a smart
device is powered by an LSH relay or actuator, but it can also change state from
another system such as Home Assistant, Shelly, ESPHome or a vendor app.

The helper does not subscribe to those systems directly. Keep that integration
in normal Node-RED nodes, then pass a clean state message into the helper.

Recommended wiring:

1. Enable **Export Internal State** and **Export Effective Config** on the
   `lsh-logic` node that owns the upstream actuator.
2. Use unique export keys when a flow has more than one `lsh-logic` node.
3. Feed external state updates into `lsh-actuator-sync`.
4. Add the target LSH device and actuator to each message, usually with a Change
   node.
5. Wire the helper output to the same MQTT output used by `lsh-logic` commands.

Default message contract:

| Property         | Meaning                                    |
| ---------------- | ------------------------------------------ |
| `msg.payload`    | Desired downstream state.                  |
| `msg.deviceId`   | Upstream LSH device id, for example `j1`.  |
| `msg.actuatorId` | Upstream LSH actuator id, for example `7`. |

The state property can be changed in the editor. Values are accepted as boolean,
`1/0`, `true/false`, `on/off` or `yes/no`.

The helper reads `homieBasePath` from the selected effective config export and
the current actuator state from the selected internal state export. If the LSH
state is already aligned, it emits nothing. If it differs, it emits:

```json
{
  "topic": "homie/5/j1/7/state/set",
  "payload": true,
  "qos": 2,
  "retain": false
}
```

The default policy ignores retained external-state messages and requires an
authoritative LSH state before commanding. Those defaults avoid surprising relay
writes after a deploy or broker replay. Disable **Require Known LSH State** only
when the external state should be applied even if the LSH registry has not
caught up.

Use **Direction** to limit which external state changes may command LSH:

| Direction       | Use when                                                                  |
| --------------- | ------------------------------------------------------------------------- |
| Sync ON and OFF | The downstream device stays powered and reachable, such as a Shelly.      |
| OFF only        | The downstream light loses power when LSH is off, such as a powered lamp. |
| ON only         | A flow should never turn an LSH actuator off.                             |

For powered smart lights, **OFF only** avoids treating a boot-time ON report as
an instruction to turn LSH on. The helper can still mirror a deliberate
downstream OFF back to LSH.

When **Require Known LSH State** is enabled and an external state arrives before
`lsh-logic` has exported an authoritative actuator snapshot, the helper does
not drop it immediately. It keeps the latest state per `deviceId/actuatorId` and
retries until **Context Ready Wait** expires. The default wait is `5000` ms. Set
it to `0` if you prefer the older fail-fast behavior.

### Multiple Actuator Sync Helpers

You can use multiple `lsh-actuator-sync` nodes in the same Node-RED runtime.
They do not share hidden runtime state; each instance reads the configured
`lsh_state` and `lsh_config` exports, then emits a command only for the
`deviceId/actuatorId` carried by the current message.

The safest pattern is one helper instance per downstream device, or one helper
per small group of downstream devices that share the same policy. Multiple
instances are useful when devices need different settings:

- **Direction**, for example `OFF only` for powered smart lights and `Sync ON
and OFF` for always-powered Shelly modules;
- **Retained Messages**, when one integration has reliable retained state and
  another does not;
- **Command Cooldown**, when some devices publish noisy state bursts;
- **State Property** and true/false parsing, when payloads come from different
  systems;
- **QoS**, when selected command paths need a stronger delivery policy.

Do not run two sync helpers that can command the same LSH `deviceId/actuatorId`
unless the duplication is intentional and externally de-duplicated. The helper
will avoid commands when LSH is already aligned, but two independent instances
watching the same target can still make the flow harder to reason about and may
emit duplicate commands during rapid state changes.

If a flow contains more than one `lsh-logic` node, give each coordinator unique
state and config export keys. Point each `lsh-actuator-sync` instance at the
matching pair so it never reads one coordinator and commands another.
