Lox Nexus

InfluxDB plugin

Writes the values of Nexus signals to InfluxDB 1, 2 or 3, for Grafana dashboards and long-term storage. Values only go out: nothing comes back to Nexus or Loxone.

What it does

  • Panel: the export's state, its target, the last success and error, and counters of written, buffered, dropped, rejected and skipped points.
  • Export: writes every value of the signals you choose under Exported signals on the instance's page (Loxone inputs and outputs, other plugins' sensors and actuators) as one point in one measurement, tagged with the signal's id, type, topic, name and owner.

Before you start

  • An InfluxDB server Nexus can reach over http or https: InfluxDB 2 or 3 (they use the version 2 write API) or InfluxDB 1.
  • InfluxDB 2: the organization, a bucket and an API token with write access to that bucket. InfluxDB 3: the database and a token that may write to it.
  • InfluxDB 1: the database, and a user and password if authentication is on.
  • After saving the instance, choose the signals under Exported signals. Until you do, nothing is written.

Configuration

Field Description
URL (url) required Address of the InfluxDB server, http or https, e.g. http://influx.local:8086. A path is kept as a prefix (https://proxy.local/influx). No user or password in the URL.
API (api) optional; default v2 InfluxDB 2 or 3 (token) (v2) or InfluxDB 1 (user and password) (v1). Choose v2 for InfluxDB 3 too.
Organization (org) required with v2 The organization's name or ID, e.g. home. InfluxDB 3 ignores it; enter any name.
Bucket (bucket) required with v2 The bucket, e.g. loxone. On InfluxDB 3 the database.
API token (token) required with v2; secret A token allowed to write to the bucket. Visible ASCII characters only, no spaces.
Database (database) required with v1 The InfluxDB 1 database, e.g. loxone.
Retention policy (retentionPolicy) optional InfluxDB 1 only. Leave empty for the database's default, e.g. autogen.
User (username) optional InfluxDB 1 only. Leave empty when authentication is off. Visible ASCII characters, no spaces or colons.
Password (password) optional; secret InfluxDB 1 only: the user's password. Ignored without a user.
Measurement (measurement) optional; default lox_nexus All points go to this measurement; signals are told apart by tags. Must not start with #.
Tag: topic (tagTopic) optional; default on Add the signal's bus topic as tag topic, e.g. loxone/home/in/temperature.
Tag: name (tagLabel) optional; default on Add the signal's name as tag label, e.g. Living room temperature.
Tag: owner (tagOwner) optional; default on Add the Miniserver or plugin instance as tag owner (miniserver:<id> or plugin:<id>).
Write interval (s) (flushSeconds) optional; default 5 1 to 3600. Values are collected and written at least this often.
Batch size (batchSize) optional; default 5000 1 to 10 000 points per request; a full batch is written right away. Not more than the buffer.
Buffer (points) (maxBufferedPoints) optional; default 25000 100 to 50 000. Points kept while InfluxDB is unreachable; beyond that the oldest are dropped.
Write initial values (writeInitial) optional; default on Also write the current value Nexus sends when watching starts and when you add a signal to the choice. Off: only changes.
Compress (gzip) (gzip) optional; default on Send requests gzip-compressed. Turn off only for a server or proxy that can't take it.

A missing field of the chosen API stops the instance from starting, with a message naming it.

Sensors and actuators

The plugin has no sensors, actuators or catalog: it only exports the signals you choose.

Good to know

  • Filter dashboards by the tag signal_id: it survives renames, while label and topic change when a signal or its owner is renamed.
  • The buffer is in memory. While InfluxDB is down, points are kept up to Buffer (points), then the oldest are dropped; whatever is unwritten at a stop or restart is lost.
  • A wrong URL or a stopped server shows as degraded shortly after the start. Wrong credentials or a missing bucket show with the first write; points stay buffered and the write is tried again every minute.
  • Data InfluxDB refuses (for example older than the retention period) is dropped, counted as rejected in the panel and logged at most once a minute; writing goes on.
  • The plugin sees whatever you choose, alarm states or presence included, and stores it in InfluxDB. Use https when InfluxDB is on another host, and give the token write access to one bucket only.

More

The full reference (data model and fields per value type, line protocol, batching and retries, the panel, Grafana queries, limits, security and development) is in REFERENCE.md next to this file, and on the plugin's page in the Nexus documentation.

Choosing the signals

The plugin is an exporter. Which signals it writes is chosen on the instance's page under Exported signals: all of them, whole Miniservers or plugin instances, or single signals. The config only says where and how to write. Until you choose, nothing is written.

Test connection in the settings form asks the server's health endpoint (/health, or /ping for version 1), which touches no data. It shows the address works; whether the token or user may write shows at the first write.

Config details

  • org, bucket, token, database, retentionPolicy and username are trimmed; password and measurement are taken as entered. Text fields take at most 200 characters, the token and the password at most 1000.
  • The fields the chosen API needs are checked with the config: with v2 Organization, Bucket and API token, with v1 Database. A missing one fails the start with invalid config: … and a message such as Organization is required for InfluxDB 2 or 3 (token). Batch size larger than Buffer (points) is refused the same way.
  • A query string or #… in the URL is ignored; a path is kept as a prefix.
  • Tokens and user names a header can't carry (spaces, line breaks, non-ASCII characters) are refused by the config check. A user name must not contain a colon, since basic auth splits user and password at the first one.

Requests

API Write Health check Auth
v2 (InfluxDB 2 and 3) POST <url>/api/v2/write?org=…&bucket=…&precision=ms GET <url>/health Authorization: Token …
v1 POST <url>/write?db=…&rp=…&precision=ms (rp only when set) GET <url>/ping basic auth, only when a user is set

The body is line protocol (text/plain; charset=utf-8), gzip-compressed unless Compress (gzip) is off. Every request times out after 15 seconds.

At start the plugin asks the health endpoint once, in the background. When that fails (wrong URL, server down, an answer of 300 or above) and nothing was written yet, the instance shows degraded with the reason right away instead of with the first values; the next answered write clears it. The check doesn't test the credentials or the bucket: the first write does.

Data model

One measurement (lox_nexus by default), one point per value, timestamped with the time the value was on the bus (millisecond precision).

Tags (sorted by key, as InfluxDB recommends)

Tag Always Example
signal_id yes 4f9c2a1e-…: stable, survives renames. Filter by this in dashboards that must not break.
value_type yes number, boolean, string, t5, singleChannel, tunableWhite, rgbw (the value's own type)
topic with tagTopic loxone/home/in/temperature. Changes when the signal or its owner is renamed.
label with tagLabel Living room temperature, the signal's name. Changes when it is renamed.
owner with tagOwner miniserver:<id> or plugin:<id>, the Miniserver or plugin instance it belongs to; left out for a signal without owner

Fields are fixed per value type, so one field name always has one type. Two signals, or a signal whose type changes, can never cause a field type conflict in InfluxDB:

Value type Fields (all floats unless noted)
number value
boolean value: 0 or 1
t5 value: the buttons bitmask
string text (string)
singleChannel brightness, fade_time
tunableWhite brightness, temperature, fade_time
rgbw red, green, blue, white, fade_time
null no point

A forced value (set by hand in Nexus, not sent by the device or Miniserver) also gets the boolean field forced=true. Commands sent to actuators are written like states.

Initial values. When watching starts, and later for each signal you add to the choice, Nexus first sends the current value of that signal (actuators have one only when forced). With Write initial values off these are left out silently; only the changes after them are written.

Line protocol

  • In the measurement, tag keys, tag values and field keys, backslashes, commas and spaces are escaped, and equal signs too except in the measurement. Control characters (line breaks, tabs) become spaces there, since line protocol can't escape them.
  • Tags with an empty value are left out (line protocol can't carry them).
  • String fields keep everything, line breaks included; quotes and backslashes are escaped.
  • Numbers are written without the i suffix, so they are floats. NaN and Infinity can't be written: such a field is left out, and a point left without fields is skipped.

Example lines:

lox_nexus,label=Living\ room,owner=miniserver:ms1,signal_id=t1,topic=loxone/home/in/temperature,value_type=number value=21.5 1793606400000
lox_nexus,label=Kitchen\ light,owner=plugin:hue,signal_id=l1,topic=hue/bridge/actuator/kitchen,value_type=rgbw red=100,green=50,blue=0,white=10,fade_time=1 1793606400500

Writing and retries

Values are buffered in memory and written in batches, one request at a time: every flushSeconds, and right away when batchSize points are waiting. Receiving values never waits for InfluxDB, so a slow or unreachable server doesn't slow Nexus down.

InfluxDB answers What happens
204 (any 2xx) written; a degraded status is cleared
429 normal throttling, not degraded: the batch is sent again when Retry-After says (1 s to 10 min), else after 1 s, 2 s, 4 s … up to 60 s
503 with Retry-After status degraded; the batch is sent again when the server asks (1 s to 10 min)
other 5xx, 408, network error, timeout (15 s) status degraded; the batch is kept and sent again after 1 s, 2 s, 4 s … up to 60 s
413 status degraded: "batch too large (HTTP 413); lower the batch size". The batch is split in half and sent again at once; batches stay at most that size until the instance restarts. A single point that is still too large is rejected.
400 and other 4xx (422: a field type conflict, or older than the retention period) the data itself is wrong: the batch is dropped and counted as rejected, the reason (the start of InfluxDB's answer, at most 500 characters) is logged at most once a minute, and writing goes on. The server answered, so a degraded status is cleared.
401, 403 status degraded: "unauthorized", naming the token or the user and password to check. Points keep being buffered; tried again every 60 s.
404 status degraded: "bucket or database not found"; as above
3xx status degraded: the URL redirects (the message names the new address); set the URL to it. Redirects are never followed, so a token never goes to another address. As above.

While a batch is retried nothing else is sent, so points arrive in order. After an outage the backlog is sent full batch after full batch without waiting for the interval; a last, smaller batch goes with the next interval. A failure is logged when its reason changes, not on every retry, and InfluxDB answers again once it does.

On stop the plugin waits for a running request, then sends what is left batch by batch until a write fails, all within 1.5 seconds. Failures during the stop don't change the status. What is still unwritten then is lost, and the log says how many points.

Partial writes. InfluxDB may store the valid lines of a batch it answers 400 or 422 to. When the answer says how many points were dropped (partial write: … dropped=N, or InfluxDB 3's JSON list of failed lines), only those count as rejected and the rest as written. An answer that doesn't say, or whose count lies beyond the 500 characters kept, counts the whole batch as rejected. The fixed field types per value type make such answers rare.

Panel

Two sections. The counters live in memory and start at zero with every start.

  • Export: State (waiting for values, ok, ok, the last batch was rejected, or what is being retried and when), Target (the URL with org and bucket, or database, retention policy and user; never the token or password), Watched signals (none chosen yet until you choose), Last success, Last error (time and reason) and Last latency.
  • Points: Written, Buffered (of the capacity), Dropped (buffer full), Dropped by Nexus (export behind), Rejected by InfluxDB, and Skipped (null or unknown signal), which also counts points without a writable field.

Retry now (Export section) writes the batch waiting for a retry at once instead of after the delay, e.g. after fixing the token, bucket or database. Without a waiting batch it says so.

Debug log

With Debug for 15 min on the instance's log card, the log also gets a line per written batch: the number of points, the latency and the outcome, with InfluxDB's reason when it wasn't ok. Never the token or password. A rejected batch's reason is InfluxDB's answer, which may quote the line it could not parse, as the warning does.

Grafana

InfluxDB 2 (Flux), by signal id (stable) or by name:

from(bucket: "loxone")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r._measurement == "lox_nexus" and r._field == "value")
  |> filter(fn: (r) => r.signal_id == "4f9c2a1e-…")   // or: r.label == "Living room temperature"
  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)
  |> keep(columns: ["_time", "_value", "label"])

InfluxDB 1 (InfluxQL):

SELECT mean("value") FROM "lox_nexus"
WHERE "label" = 'Living room temperature' AND $timeFilter
GROUP BY time($__interval), "label" fill(none)

For an on/off signal use last("value") or max("value") instead of mean. To see every signal of a Miniserver: WHERE "owner" = 'miniserver:<id>', grouped by "label". InfluxDB 3 takes SQL or InfluxQL in Grafana; the InfluxQL query above works there too.

Limits

  • The buffer is in memory and bounded by maxBufferedPoints and by 32 million characters of line protocol, whichever comes first. When it is full the oldest waiting points are dropped and counted (Dropped (buffer full) in the panel, and a warning in the log at most once a minute). A batch being sent or retried is never dropped. A typical line (UUID tags, a topic and a name) is about 250 characters and takes about 280 bytes of memory (more with non-Latin characters in names, which double a line's size). The worker has a 128 MB heap, which is why the buffer is capped at 50 000 points. Nothing is kept across restarts.
  • If the plugin itself falls behind, Nexus drops the oldest values before delivering them; these are counted as Dropped by Nexus and warned about like a full buffer. Mapping and buffering are fast, so this should not happen.
  • Integers are written as floats: one value field for all numeric types avoids type conflicts.
  • RGBW flags are not written.
  • Points of the same signal with the same millisecond overwrite each other in InfluxDB.
  • Values of a signal that isn't in the watch list are skipped (Nexus doesn't send such values; this is a safeguard).

Security

  • The token and password are secret config fields: never sent back to the UI, never logged, never in the panel. The v1 password goes in a basic auth header, not in the URL, where it would end up in access logs. Errors from building a request are reported in fixed words, never with the request's headers.
  • Redirects are never followed, so the token is never sent to another address. Response bodies are read only up to 4 KB, and at most 500 characters of them are kept for messages.
  • Use https when InfluxDB is not on the same host; the token travels with every request.
  • Give the token write access to one bucket only.
  • This plugin sees the values of other systems: whatever signals you choose, for example alarm states or presence, and writes them to InfluxDB. Choose only what you want stored there, and protect the database like the data in it. Secrets never travel on the bus.

Development

npm test                                       # from the repo root; tests live next to the code in src/
npm run build -w @lox-nexus/plugin-influxdb    # dist/plugin.js, one ESM bundle (esbuild)

Tests run against a fake InfluxDB (src/test-support/FakeInflux.ts, node:http on 127.0.0.1) that records the requests and answers from a script; no real server is contacted.