> ## Documentation Index
> Fetch the complete documentation index at: https://docs.coreflux.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Litmus Edge

> Client-mode REST API connector for Litmus Edge 4.0.x: get an OAuth2 token, list DeviceHub devices and tags, create, update and delete tags, start and stop devices, toggle device data storage, check service versions and the time-series DB, and list, start and stop Node-RED flows and integrations

Technical Preview

## Overview

Litmus Edge is an industrial edge data platform: it connects PLCs and other devices through DeviceHub, stores tag data locally, runs Node-RED flows and analytics, and forwards data through integrations. The **LitmusEdge** connector lets a Coreflux broker manage and query an edge over its REST and GraphQL API (`https://<edge>/...`, the same address as the web UI).

Each event publishes the edge's reply on `<topic>/response`.

| Event | Topic | Request | Notes |
| - | - | - | - |
| GetAccessToken | `historian/litmus-edge/auth-token/create` | `POST /auth/v3/oauth/token` | OAuth2 client credentials; returns access\_token and expires\_in |
| GetDeviceHubVersion | `historian/litmus-edge/devicehub-version/get` | `GET /devicehub/version` | DeviceHub version and git revision (liveness probe) |
| GetDeviceManagementVersion | `historian/litmus-edge/dm-version/get` | `GET /dm/version` | Device Management service version |
| ListDevices | `historian/litmus-edge/device/list` | `POST /devicehub/v2` (query ListDevices) | Every DeviceHub device (ID, Name, DriverID, params) |
| StartDevices | `historian/litmus-edge/device/start` | `POST /devicehub/v2` (mutation StartDevices) | Start polling one or more devices (idempotent) |
| StopDevices | `historian/litmus-edge/device/stop` | `POST /devicehub/v2` (mutation StopDevices) | Stop polling one or more devices |
| SetDeviceDataStorage | `historian/litmus-edge/device-storage/update` | `PUT /stats/devices/{device_id}` | Enable or disable persistence of a device's tags (body ID, name, retentionHours, saveAll) |
| ListAllTags | `historian/litmus-edge/tag/list` | `POST /devicehub/v2` (query ListRegistersFromAllDevices) | Tags across all devices (Limit / SkipCount / TagPattern) |
| ListDeviceTags | `historian/litmus-edge/device-tag/list` | `POST /devicehub/v2` (query ListRegisters) | Tags of one device (DeviceID, Limit / SkipCount) |
| CreateTags | `historian/litmus-edge/tag/create` | `POST /devicehub/v2` (mutation CreateRegisters) | Create one or more tags on a device |
| UpdateTags | `historian/litmus-edge/tag/update` | `POST /devicehub/v2` (mutation UpdateRegisters) | Replace tag configuration by ID |
| DeleteTags | `historian/litmus-edge/tag/delete` | `POST /devicehub/v2` (mutation DeleteRegisters) | Delete tags by ID (unknown IDs are ignored) |
| GetTsdbInfo | `historian/litmus-edge/tsdb/get` | `GET /stats/tsdb` | Time-series DB state (is\_enabled) and source |
| ListFlows | `historian/litmus-edge/flow/list` | `GET /flows-manager/flows` | Node-RED flow instances (id, name, tabs) |
| StartFlow | `historian/litmus-edge/flow/start` | `PUT /flows-manager/flows/{flow_id}/start` | Start a flow instance (204) |
| StopFlow | `historian/litmus-edge/flow/stop` | `PUT /flows-manager/flows/{flow_id}/stop` | Stop a flow instance (204) |
| ListIntegrations | `historian/litmus-edge/integration/list` | `GET /cc/instances` | Integration (cloud / MQTT / Kafka ...) instances |

DeviceHub is GraphQL: every DeviceHub event posts to `/devicehub/v2`, and you send the GraphQL document in the payload as `body` (`{"query": "...", "variables": {...}}`). The event name tells you which query or mutation it is meant for.

> [!NOTE]
>
> **Live tag values and tag writes are not REST in Litmus Edge.** Litmus documents tag writes only on the edge's NATS bus (`devicehub.write.<device>.<tag>`), and live values stream on `devicehub.alias.<device>.<tag>`. To move live data between Litmus Edge and Coreflux, use a Litmus Edge MQTT integration pointed at this broker. Use this connector for configuration and control.

## Prerequisites

1. Litmus Edge 4.0.x, reachable from the broker host on HTTPS (port 443 on an appliance or VM; a Docker install usually publishes it on host port 8443).
2. An OAuth2 client (client ID and secret) or a REST API token from **System** > **Access Control** > **Tokens**, with permission for DeviceHub, DataHub and Flows Manager.
3. The edge certificate is self-signed by default. Install the edge CA on the broker host (download it from **System** > **Network** > **Certificates**), or replace it with a trusted certificate.
4. Store the credentials in Hub secrets, for example `LITMUS_CLIENT_ID` and `LITMUS_CLIENT_SECRET`, or `LITMUS_API_TOKEN`.

## Authentication

Every event except `GetAccessToken` takes the full `Authorization` header value in the payload as `authorization`:

* **OAuth2 client credentials:** publish `{"client_id": "...", "client_secret": "..."}` to `historian/litmus-edge/auth-token/create`. The reply holds `access_token` and `expires_in`. Send `"authorization": "Bearer <access_token>"`.
* **REST API token:** send `"authorization": "Basic <base64 of token:>"` (the token is the username, the password is empty).

> [!WARNING]
>
> Token lifetime depends on the edge. Litmus reports `expires_in` values as short as 300 seconds, so refresh the token on a timer shorter than `expires_in` (the example below refreshes every 4 minutes).

## Configuration

Replace `YOUR_LITMUS_EDGE_HOST` in `BASE_ADDRESS` with the edge address, for example `https://10.0.0.20` or `https://10.0.0.20:8443` for Docker.

## Examples

List devices:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// publish to historian/litmus-edge/device/list
{ "authorization": "Bearer <token>", "body": { "query": "query ListDevices { ListDevices { ID Name DriverID } }" } }
// historian/litmus-edge/device/list/response
{ "data": { "ListDevices": [{ "ID": "41FBD12E-...", "Name": "Line1 PLC", "DriverID": "2AF1FA08-..." }] } }
```

List the tags of one device:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// publish to historian/litmus-edge/device-tag/list
{ "authorization": "Bearer <token>",
  "body": { "query": "query ListRegisters($input: ListRegistersRequest!) { ListRegisters(input: $input) { Registers { ID TagName ValueType } TotalCount Last } }",
            "variables": { "input": { "DeviceID": "<device uuid>", "Limit": 100, "SkipCount": 0 } } } }
```

Create a tag:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// publish to historian/litmus-edge/tag/create
{ "authorization": "Bearer <token>",
  "body": { "query": "mutation CreateRegisters($input: CreateRegistersRequest!) { CreateRegisters(input: $input) { ID TagName } }",
            "variables": { "input": { "Registers": [{ "DeviceID": "<device uuid>", "Name": "40001", "TagName": "Line1Temperature", "ValueType": "float64",
                                                  "Properties": [{ "Key": "pollingInterval", "Value": "1000" }] }] } } } }
```

Stop and start a device:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// publish to historian/litmus-edge/device/stop (or device/start with StartDevices)
{ "authorization": "Bearer <token>",
  "body": { "query": "mutation StopDevices($input: StopDevicesRequest!) { StopDevices(input: $input) }",
            "variables": { "input": { "IDs": ["<device uuid>"] } } } }
```

Restart a Node-RED flow instance:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// publish to historian/litmus-edge/flow/start
{ "authorization": "Bearer <token>", "flow_id": 1 }
// reply: HTTP 204, empty body
```

Register properties (`Name`, `Properties` keys) depend on the device's driver; Litmus lists them per driver in its DeviceHub register reference.

### Example actions

Keep GraphQL documents in a retained topic (for example `config/litmus-edge/list-devices` holding `{"query": "query ListDevices { ListDevices { ID Name DriverID } }"}`) and read them with `GET TOPIC`, so the GraphQL text never sits inside a LoT JSON literal.

```lot theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION LitmusRefreshToken
ON EVERY 4 MINUTES DO
    SET "cid" WITH GET SECRET "LITMUS_CLIENT_ID"
    SET "csecret" WITH GET SECRET "LITMUS_CLIENT_SECRET"
    PUBLISH TOPIC "historian/litmus-edge/auth-token/create" WITH { "client_id": {cid}, "client_secret": {csecret} }

DEFINE ACTION LitmusKeepToken
ON TOPIC "historian/litmus-edge/auth-token/create/response" DO
    SET "token" WITH GET JSON "access_token" IN PAYLOAD AS STRING
    KEEP TOPIC "internal/litmus-edge/authorization" WITH "Bearer " + {token}

DEFINE ACTION LitmusListDevices
ON EVERY 1 HOUR DO
    SET "auth" WITH GET TOPIC "internal/litmus-edge/authorization"
    SET "gql" WITH GET TOPIC "config/litmus-edge/list-devices"
    PUBLISH TOPIC "historian/litmus-edge/device/list" WITH { "authorization": {auth}, "body": {gql} }

DEFINE ACTION LitmusHealthCheck
ON EVERY 5 MINUTES DO
    SET "auth" WITH GET TOPIC "internal/litmus-edge/authorization"
    PUBLISH TOPIC "historian/litmus-edge/devicehub-version/get" WITH { "authorization": {auth} }

DEFINE ACTION LitmusRestartFlowOnAlarm
ON TOPIC "plant/line1/flow-restart" DO
    SET "auth" WITH GET TOPIC "internal/litmus-edge/authorization"
    SET "flow" WITH PAYLOAD AS INT
    PUBLISH TOPIC "historian/litmus-edge/flow/start" WITH { "authorization": {auth}, "flow_id": {flow} }
```

## Responses and errors

| Case | Answer |
| - | - |
| Wrong client ID or secret on `GetAccessToken` | `401` |
| Missing, wrong or expired token on REST endpoints | `401` |
| GraphQL errors (bad input, unknown device, no permission) | HTTP `200` with an `errors` array; check `errors[].extensions.code` (`UNAUTHENTICATED`, `FORBIDDEN`, `BAD_USER_INPUT`, `NOT_FOUND`) |
| `StartFlow`, `StopFlow` | `204`, empty body |
| `DeleteTags` with unknown IDs | Ignored, no error |

Always check the `errors` key in DeviceHub responses, not only the HTTP status.

## Testing

Checked 2026-10-05 against Litmus's Litmus Edge 4.0.x API reference (17/17 events, endpoints, methods and GraphQL operations) and a local mock edge (19/19 calls, plus wrong secret `401`, wrong token `401`, API token over Basic, missing `DeviceID` `BAD_USER_INPUT`, unknown flow `404`). A run against a real Litmus Edge and a broker MQTT round-trip are still open. See `TEST.md` in the app package for the smoke script.

## Not covered

* Tag value writes and live values (NATS bus only, see the note above). Use a Litmus Edge MQTT integration.
* DeviceHub subscriptions (`RegisterStatus`, `DeviceConnectionStatus`), which need a GraphQL websocket.
* Browse, asset discovery, digital twins, analytics, applications, OPC UA server, system administration (network, backups, certificates, RBAC) and Litmus Edge Manager (LEM) fleet APIs.
* Litmus Edge 3.16.x endpoints.

## Related

* [REST API connectors](/latest/lot-language/routes/rest-api-routes)
* [REST API app catalog](/latest/lot-language/routes/rest-api-apps/index)
* [Litmus Edge API reference](https://api.litmus.io/le/)
* [Litmus API portal: your first API call](https://docs.litmus.io/api-docs/first-api-call)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.