feat(output): alwaysEmit fields, drop undefined/empty Influx tags, time-based movement re-basing
- OutputUtils: new `alwaysEmit` option exempts named fields from delta compression so steady-state values (e.g. ctrl) trace continuously. - flattenTags now drops null/undefined/empty-string tag values, fixing literal `category="undefined"` tags that split every Grafana series in two. - BaseNodeAdapter wires `static alwaysEmitFields` from the subclass. - movementManager: track position by elapsed wall-time and capture partial progress on abort, so a fast-re-commanding parent can't freeze an actuator at its start position. - Tests for the above. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,16 @@ const { getFormatter } = require('./formatters');
|
||||
|
||||
//this class will handle the output events for the node red node
|
||||
class OutputUtils {
|
||||
constructor() {
|
||||
// `options.alwaysEmit` is an optional list of field keys that bypass delta
|
||||
// compression: they are re-emitted on every tick even when unchanged. Use it
|
||||
// sparingly for slowly-varying values that must still trace as a continuous
|
||||
// line downstream (e.g. a pump's realized control position `ctrl`, which sits
|
||||
// constant in steady state and otherwise produces ~1 point per long stretch —
|
||||
// invisible in a Grafana timeseries with createEmpty:false). Defaults to none,
|
||||
// so existing nodes keep pure delta-compression behaviour.
|
||||
constructor(options = {}) {
|
||||
this.output = {};
|
||||
this.alwaysEmit = new Set(options.alwaysEmit || []);
|
||||
}
|
||||
|
||||
checkForChanges(output, format) {
|
||||
@@ -13,7 +21,9 @@ class OutputUtils {
|
||||
this.output[format] = this.output[format] || {};
|
||||
const changedFields = {};
|
||||
for (const key in output) {
|
||||
if (Object.prototype.hasOwnProperty.call(output, key) && output[key] !== this.output[format][key]) {
|
||||
if (!Object.prototype.hasOwnProperty.call(output, key)) continue;
|
||||
const forced = this.alwaysEmit.has(key) && output[key] !== undefined;
|
||||
if (forced || output[key] !== this.output[format][key]) {
|
||||
let value = output[key];
|
||||
// For fields: if the value is an object (and not a Date), stringify it.
|
||||
if (value !== null && typeof value === 'object' && !(value instanceof Date)) {
|
||||
@@ -79,7 +89,13 @@ class OutputUtils {
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
const value = obj[key];
|
||||
if (value !== null && typeof value === 'object' && !(value instanceof Date)) {
|
||||
// Skip tags that carry no information. When a config field is unset,
|
||||
// extractRelevantConfig hands us `undefined`; stringifying that wrote
|
||||
// literal `category="undefined"` / `geoLocation="undefined"` tags that
|
||||
// clutter every Grafana legend and needlessly inflate tag cardinality.
|
||||
// Drop null / undefined / empty-string before they reach InfluxDB.
|
||||
if (value === null || value === undefined || value === '') continue;
|
||||
if (typeof value === 'object' && !(value instanceof Date)) {
|
||||
// Recursively flatten the nested object.
|
||||
const flatChild = this.flattenTags(value);
|
||||
for (const childKey in flatChild) {
|
||||
|
||||
Reference in New Issue
Block a user