- 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>
108 lines
4.1 KiB
JavaScript
108 lines
4.1 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
const OutputUtils = require('../src/helper/outputUtils.js');
|
|
|
|
const config = {
|
|
functionality: { softwareType: 'measurement', role: 'sensor' },
|
|
general: { id: 'abc', unit: 'mbar' },
|
|
asset: {
|
|
uuid: 'u1',
|
|
tagCode: 't1',
|
|
geoLocation: { lat: 51.6, lon: 4.7 },
|
|
category: 'measurement',
|
|
type: 'pressure',
|
|
model: 'M1',
|
|
},
|
|
};
|
|
|
|
test('process format emits message with changed fields only', () => {
|
|
const out = new OutputUtils();
|
|
|
|
const first = out.formatMsg({ a: 1, b: 2 }, config, 'process');
|
|
assert.equal(first.topic, 'measurement_abc');
|
|
assert.deepEqual(first.payload, { a: 1, b: 2 });
|
|
|
|
const second = out.formatMsg({ a: 1, b: 2 }, config, 'process');
|
|
assert.equal(second, null);
|
|
|
|
const third = out.formatMsg({ a: 1, b: 3, c: { x: 1 } }, config, 'process');
|
|
assert.deepEqual(third.payload, { b: 3, c: JSON.stringify({ x: 1 }) });
|
|
});
|
|
|
|
test('alwaysEmit fields bypass delta compression (re-emitted while unchanged)', () => {
|
|
const out = new OutputUtils({ alwaysEmit: ['ctrl'] });
|
|
|
|
const first = out.formatMsg({ ctrl: 40, flow: 12 }, config, 'influxdb');
|
|
assert.deepEqual(first.payload.fields, { ctrl: 40, flow: 12 });
|
|
|
|
// flow unchanged → dropped; ctrl unchanged but forced → still emitted.
|
|
const second = out.formatMsg({ ctrl: 40, flow: 12 }, config, 'influxdb');
|
|
assert.deepEqual(second.payload.fields, { ctrl: 40 });
|
|
|
|
// ctrl changed → emitted with its new value.
|
|
const third = out.formatMsg({ ctrl: 41, flow: 12 }, config, 'influxdb');
|
|
assert.deepEqual(third.payload.fields, { ctrl: 41 });
|
|
});
|
|
|
|
test('alwaysEmit is per-format and does not force a missing/undefined field', () => {
|
|
const out = new OutputUtils({ alwaysEmit: ['ctrl'] });
|
|
// ctrl absent from the output → nothing to force; with no other change the
|
|
// message is suppressed as usual.
|
|
out.formatMsg({ flow: 5 }, config, 'influxdb');
|
|
assert.equal(out.formatMsg({ flow: 5 }, config, 'influxdb'), null);
|
|
});
|
|
|
|
test('default OutputUtils keeps pure delta compression (no alwaysEmit)', () => {
|
|
const out = new OutputUtils();
|
|
out.formatMsg({ ctrl: 40 }, config, 'influxdb');
|
|
assert.equal(out.formatMsg({ ctrl: 40 }, config, 'influxdb'), null);
|
|
});
|
|
|
|
test('influx format flattens tags and stringifies tag values', () => {
|
|
const out = new OutputUtils();
|
|
const msg = out.formatMsg({ value: 10 }, config, 'influxdb');
|
|
|
|
assert.equal(msg.topic, 'measurement_abc');
|
|
assert.equal(msg.payload.measurement, 'measurement_abc');
|
|
assert.equal(msg.payload.tags.geoLocation_lat, '51.6');
|
|
assert.equal(msg.payload.tags.geoLocation_lon, '4.7');
|
|
assert.equal(msg.payload.tags.tagcode, 't1');
|
|
assert.ok(msg.payload.timestamp instanceof Date);
|
|
});
|
|
|
|
test('influx format omits tags whose config value is unset', () => {
|
|
const out = new OutputUtils();
|
|
// No asset block at all: uuid/tagcode/geoLocation/category/type/model are
|
|
// all undefined and must NOT appear as `="undefined"` tags.
|
|
const sparse = {
|
|
functionality: { softwareType: 'measurement' },
|
|
general: { id: 'abc' },
|
|
};
|
|
const msg = out.formatMsg({ value: 10 }, sparse, 'influxdb');
|
|
|
|
for (const t of ['geoLocation', 'category', 'type', 'model', 'uuid', 'tagcode', 'unit', 'role']) {
|
|
assert.ok(!(t in msg.payload.tags), `tag "${t}" should be omitted when unset, got "${msg.payload.tags[t]}"`);
|
|
}
|
|
// Tags that DO have values still come through.
|
|
assert.equal(msg.payload.tags.id, 'abc');
|
|
assert.equal(msg.payload.tags.softwareType, 'measurement');
|
|
// Nothing should stringify to the literal "undefined".
|
|
for (const v of Object.values(msg.payload.tags)) {
|
|
assert.notEqual(v, 'undefined');
|
|
}
|
|
});
|
|
|
|
test('influx format drops empty-string tag values too', () => {
|
|
const out = new OutputUtils();
|
|
const cfg = {
|
|
functionality: { softwareType: 'pump', role: '' },
|
|
general: { id: 'p1' },
|
|
asset: { category: '', model: 'M9' },
|
|
};
|
|
const msg = out.formatMsg({ value: 1 }, cfg, 'influxdb');
|
|
assert.ok(!('role' in msg.payload.tags));
|
|
assert.ok(!('category' in msg.payload.tags));
|
|
assert.equal(msg.payload.tags.model, 'M9');
|
|
});
|