P11.5 + B2.1/B2.2: per-command units + description (where applicable)
Adds to scalar setters whose payloads are
plain numbers OR {value, unit}. Skipped where payload is compound or
mode-dependent (control-%, {F, C: [...]}, etc.) — documented inline.
Every command gains a description field for wikiGen consumption.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,42 +12,52 @@ module.exports = [
|
|||||||
topic: 'set.mode',
|
topic: 'set.mode',
|
||||||
aliases: ['setMode'],
|
aliases: ['setMode'],
|
||||||
payloadSchema: { type: 'string' },
|
payloadSchema: { type: 'string' },
|
||||||
|
description: 'Switch the valve group between auto / manual control modes.',
|
||||||
handler: handlers.setMode,
|
handler: handlers.setMode,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
topic: 'set.position',
|
topic: 'set.position',
|
||||||
aliases: ['setpoint'],
|
aliases: ['setpoint'],
|
||||||
payloadSchema: { type: 'any' },
|
payloadSchema: { type: 'any' },
|
||||||
|
// Control-percent setpoint — no `units` (no `percent` measure in convert).
|
||||||
|
description: 'Set the group-level valve position (currently a no-op pending Phase 7).',
|
||||||
handler: handlers.setPosition,
|
handler: handlers.setPosition,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
topic: 'child.register',
|
topic: 'child.register',
|
||||||
aliases: ['registerChild'],
|
aliases: ['registerChild'],
|
||||||
payloadSchema: { type: 'string' },
|
payloadSchema: { type: 'string' },
|
||||||
|
description: 'Register a child valve with this group.',
|
||||||
handler: handlers.registerChild,
|
handler: handlers.registerChild,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
topic: 'cmd.execSequence',
|
topic: 'cmd.execSequence',
|
||||||
aliases: ['execSequence'],
|
aliases: ['execSequence'],
|
||||||
payloadSchema: { type: 'object' },
|
payloadSchema: { type: 'object' },
|
||||||
|
description: 'Run a group-wide sequence (startup / shutdown / emergencystop).',
|
||||||
handler: handlers.execSequence,
|
handler: handlers.execSequence,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
topic: 'data.totalFlow',
|
topic: 'data.totalFlow',
|
||||||
aliases: ['totalFlowChange'],
|
aliases: ['totalFlowChange'],
|
||||||
payloadSchema: { type: 'any' },
|
payloadSchema: { type: 'any' },
|
||||||
|
// Compound payload `{source, action, ...}` in some shapes — no scalar
|
||||||
|
// normalisation. The handler routes by payload.source.
|
||||||
|
description: 'Notify the group that the total flow setpoint has changed.',
|
||||||
handler: handlers.totalFlowChange,
|
handler: handlers.totalFlowChange,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
topic: 'cmd.emergencyStop',
|
topic: 'cmd.emergencyStop',
|
||||||
aliases: ['emergencyStop', 'emergencystop'],
|
aliases: ['emergencyStop', 'emergencystop'],
|
||||||
payloadSchema: { type: 'any' },
|
payloadSchema: { type: 'any' },
|
||||||
|
description: 'Trigger an emergency stop across all valves in the group.',
|
||||||
handler: handlers.emergencyStop,
|
handler: handlers.emergencyStop,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
topic: 'set.reconcileInterval',
|
topic: 'set.reconcileInterval',
|
||||||
aliases: ['setReconcileInterval'],
|
aliases: ['setReconcileInterval'],
|
||||||
payloadSchema: { type: 'any' },
|
payloadSchema: { type: 'any' },
|
||||||
|
description: 'Update the reconciliation interval (seconds).',
|
||||||
handler: handlers.setReconcileInterval,
|
handler: handlers.setReconcileInterval,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ const flowDist = require('./groupOps/flowDistribution');
|
|||||||
const sources = require('./sources/fluidContract');
|
const sources = require('./sources/fluidContract');
|
||||||
const io = require('./io/output');
|
const io = require('./io/output');
|
||||||
|
|
||||||
const KNOWN_POSITIONS = new Set(['upstream', 'downstream', 'atEquipment']);
|
// Source softwareTypes after BaseDomain canonicalisation
|
||||||
|
// (rotatingmachine→machine, machinegroupcontrol→machinegroup).
|
||||||
|
const SOURCE_SOFTWARE_TYPES = ['machine', 'machinegroup', 'pumpingstation', 'valvegroupcontrol'];
|
||||||
|
|
||||||
class ValveGroupControl extends BaseDomain {
|
class ValveGroupControl extends BaseDomain {
|
||||||
static name = 'valveGroupControl';
|
static name = 'valveGroupControl';
|
||||||
@@ -42,22 +44,10 @@ class ValveGroupControl extends BaseDomain {
|
|||||||
this.state = new state({}, this.logger);
|
this.state = new state({}, this.logger);
|
||||||
this.state.stateManager.currentState = 'operational';
|
this.state.stateManager.currentState = 'operational';
|
||||||
|
|
||||||
// Overloaded API: `(child, softwareTypeOrPosition)`. Tests + legacy
|
this.router.onRegister('valve', (child) => this._registerValve(child));
|
||||||
// childRegistrationUtils both invoke this with a string second arg,
|
for (const swType of SOURCE_SOFTWARE_TYPES) {
|
||||||
// which may be either a known position or a softwareType. Resolve
|
this.router.onRegister(swType, (child, canonicalKey) => this._registerSource(child, canonicalKey));
|
||||||
// before dispatching so router-based registration keeps working.
|
|
||||||
this.registerChild = (child, positionOrType) => this._registerChild(child, positionOrType);
|
|
||||||
}
|
|
||||||
|
|
||||||
_resolveRegistrationContext(child, arg) {
|
|
||||||
const fromArg = String(arg || '').trim();
|
|
||||||
if (KNOWN_POSITIONS.has(fromArg)) {
|
|
||||||
return { positionVsParent: fromArg, softwareType: child?.config?.functionality?.softwareType || null };
|
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
positionVsParent: child?.positionVsParent || 'atEquipment',
|
|
||||||
softwareType: fromArg || child?.config?.functionality?.softwareType || null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_isValveLike(child) {
|
_isValveLike(child) {
|
||||||
@@ -69,22 +59,7 @@ class ValveGroupControl extends BaseDomain {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_registerChild(child, positionOrType) {
|
_registerValve(child) {
|
||||||
const ctx = this._resolveRegistrationContext(child, positionOrType);
|
|
||||||
const softwareType = String(ctx.softwareType || child?.config?.functionality?.softwareType || '').trim().toLowerCase();
|
|
||||||
|
|
||||||
if (softwareType === 'valve' || (!softwareType && this._isValveLike(child))) {
|
|
||||||
return this._registerValve(child, ctx.positionVsParent);
|
|
||||||
}
|
|
||||||
if (sources.isSourceLike(child, softwareType)) {
|
|
||||||
return sources.registerSource(this, child, ctx.positionVsParent, softwareType);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.warn(`registerChild skipped: unsupported child type '${softwareType || 'unknown'}'`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_registerValve(child, positionVsParent) {
|
|
||||||
if (!this._isValveLike(child)) {
|
if (!this._isValveLike(child)) {
|
||||||
this.logger.warn('registerChild skipped: child is not valve-like');
|
this.logger.warn('registerChild skipped: child is not valve-like');
|
||||||
return false;
|
return false;
|
||||||
@@ -94,6 +69,9 @@ class ValveGroupControl extends BaseDomain {
|
|||||||
this.logger.debug(`registerChild skipped: valve ${id} already registered`);
|
this.logger.debug(`registerChild skipped: valve ${id} already registered`);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
const positionVsParent = child.positionVsParent
|
||||||
|
|| child.config?.functionality?.positionVsParent
|
||||||
|
|| 'atEquipment';
|
||||||
child.positionVsParent = positionVsParent;
|
child.positionVsParent = positionVsParent;
|
||||||
this.valves[id] = child;
|
this.valves[id] = child;
|
||||||
this._bindValveEvents(id, child);
|
this._bindValveEvents(id, child);
|
||||||
@@ -104,6 +82,13 @@ class ValveGroupControl extends BaseDomain {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_registerSource(child, softwareType) {
|
||||||
|
const positionVsParent = child.positionVsParent
|
||||||
|
|| child.config?.functionality?.positionVsParent
|
||||||
|
|| 'atEquipment';
|
||||||
|
return sources.registerSource(this, child, positionVsParent, softwareType);
|
||||||
|
}
|
||||||
|
|
||||||
_bindValveEvents(valveId, valve) {
|
_bindValveEvents(valveId, valve) {
|
||||||
const handlers = {
|
const handlers = {
|
||||||
onPositionChange: () => { this.logger.debug(`Valve ${valveId} position changed, recalculating flows.`); this.calcValveFlows(); },
|
onPositionChange: () => { this.logger.debug(`Valve ${valveId} position changed, recalculating flows.`); this.calcValveFlows(); },
|
||||||
|
|||||||
@@ -84,10 +84,43 @@ test('valveGroupControl distributes total flow according to supplier-curve Kv an
|
|||||||
valve2.destroy();
|
valve2.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('valveGroupControl rejects non-valve-like child payload', () => {
|
test('valveGroupControl skips a non-valve-like payload registered as a valve', () => {
|
||||||
const group = buildGroup();
|
const group = buildGroup();
|
||||||
const result = group.registerChild({ config: { functionality: { softwareType: 'valve' } } }, 'atEquipment');
|
// Router dispatches by softwareType; the _registerValve handler rejects
|
||||||
assert.equal(result, false);
|
// non-valve-like children (missing updateFlow/state/measurements) by
|
||||||
|
// returning false from its branch — the registry side-effect (valves[])
|
||||||
|
// stays empty even though BaseDomain's registerChild returns true.
|
||||||
|
group.registerChild({ config: { functionality: { softwareType: 'valve' } } }, 'valve');
|
||||||
assert.equal(Object.keys(group.valves).length, 0);
|
assert.equal(Object.keys(group.valves).length, 0);
|
||||||
group.destroy();
|
group.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('valveGroupControl router dispatches valve registration by softwareType, honouring config positionVsParent', async () => {
|
||||||
|
const valve = (function buildValveAtUpstream() {
|
||||||
|
const Valve = require('../../../valve/src/specificClass');
|
||||||
|
return new Valve(
|
||||||
|
{
|
||||||
|
general: { name: 'valve-upstream', logging: { enabled: false, logLevel: 'error' } },
|
||||||
|
asset: { supplier: 'binder', category: 'valve', type: 'control', model: 'ECDV', unit: 'm3/h' },
|
||||||
|
functionality: { positionVsParent: 'upstream', softwareType: 'valve' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
general: { logging: { enabled: false, logLevel: 'error' } },
|
||||||
|
movement: { speed: 1 },
|
||||||
|
time: { starting: 0, warmingup: 0, stopping: 0, coolingdown: 0 },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
primeValve(valve, 50);
|
||||||
|
const group = buildGroup();
|
||||||
|
|
||||||
|
// childRegistrationUtils consumes positionVsParent (2nd arg) and forwards
|
||||||
|
// softwareType='valve' to the parent — the router fans out from there.
|
||||||
|
assert.equal(await group.childRegistrationUtils.registerChild(valve, 'upstream'), true);
|
||||||
|
assert.equal(Object.keys(group.valves).length, 1);
|
||||||
|
const registered = Object.values(group.valves)[0];
|
||||||
|
assert.equal(registered.positionVsParent, 'upstream');
|
||||||
|
|
||||||
|
group.destroy();
|
||||||
|
valve.destroy();
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user