Files
generalFunctions/src/state/stateManager.js
znetsixe af02d36b07 feat(mgc-config + state): planner.useRendezvous schema + remaining-transition reads
Three coherent additions that the MGC rendezvous planner depends on:

- machineGroupControl.json: new `planner.useRendezvous` boolean (default
  true). Used by both `_optimalControl` and `equalFlowControl` (via the
  shared `_dispatchFlowDistribution` helper) to gate same-time-landing.

- state.js: external aborts (returnToOperational=false) bump a monotonic
  `sequenceAbortToken`. executeSequence captures it at entry and bails
  out of its for-loop if it advances mid-sequence, so a shutdown that's
  past its ramp-down step doesn't barge through stopping → coolingdown
  when a fresher demand re-engages the pump.

- stateManager.js: new `getRemainingTransitionS()` returns the seconds
  remaining in a timed state by reading the wall-clock entry timestamp.
  buildProfile() reads it so the planner can compute exact eta for a
  child that's currently mid-ladder (warmingup / starting / cooling).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:43:21 +02:00

218 lines
7.7 KiB
JavaScript

/**
* @file stateManager.js
*
* Permission is hereby granted to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to use it for personal
* or non-commercial purposes, with the following restrictions:
*
* 1. **No Copying or Redistribution**: The Software or any of its parts may not
* be copied, merged, distributed, sublicensed, or sold without explicit
* prior written permission from the author.
*
* 2. **Commercial Use**: Any use of the Software for commercial purposes requires
* a valid license, obtainable only with the explicit consent of the author.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
* OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Ownership of this code remains solely with the original author. Unauthorized
* use of this Software is strictly prohibited.
*
* @summary Class for managing state transitions and state descriptions.
* @description Class for managing state transitions and state descriptions.
* @module stateManager
* @exports stateManager
* @version 0.1.0
* @since 0.1.0
*
* Author:
* - Rene De Ren
* Email:
* - rene@thegoldenbasket.nl
*/
class stateManager {
constructor(config, logger) {
this.currentState = config.state.current;
// Wall-clock entry timestamp into currentState. Used by
// getRemainingTransitionS() so callers (e.g. MGC movement planner)
// can compute exact remaining time for timed states without
// approximating from the full configured duration.
this.stateEnteredAt = Date.now();
this.availableStates = config.state.available;
this.descriptions = config.state.descriptions;
this.logger = logger;
this.transitionTimeleft = 0;
this.transitionTimes = config.time;
// Define valid transitions (can be extended dynamically if needed)
this.validTransitions = config.state.allowedTransitions;
//runtime tracking
this.runTimeHours = 0; // cumulative runtime in hours
this.runTimeStart = null; // timestamp when active state began
//maintenance tracking
this.maintenanceTimeStart = null; //timestamp when active state began
this.maintenanceTimeHours = 0; //cumulative
// Define active states (runtime counts only in these states)
this.activeStates = config.state.activeStates;
}
getCurrentState() {
return this.currentState;
}
// Seconds remaining in the current timed state (warmingup, coolingdown,
// starting, stopping, …). Returns 0 for untimed states or once the
// configured duration has elapsed. The MGC movement planner uses this to
// compute exact rendezvous time for protected (non-interruptible) states.
getRemainingTransitionS() {
const d = this.transitionTimes?.[this.currentState] || 0;
if (d <= 0) return 0;
const elapsed = (Date.now() - this.stateEnteredAt) / 1000;
return Math.max(0, d - elapsed);
}
transitionTo(newState,signal) {
return new Promise((resolve, reject) => {
if (signal && signal.aborted) {
this.logger.debug("Transition aborted.");
return reject("Transition aborted.");
}
if (!this.isValidTransition(newState)) {
return reject(
`Invalid transition from ${this.currentState} to ${newState}. Transition not executed.`
); //go back early and reject promise
}
//Time tracking based on active states
this.handleRuntimeTracking(newState);
this.handleMaintenancetimeTracking(newState);
const transitionDuration = this.transitionTimes[this.currentState] || 0; // Default to 0 if no transition time
this.logger.debug(
`Transition from ${this.currentState} to ${newState} will take ${transitionDuration}s.`
);
if (transitionDuration > 0) {
const timeoutId = setTimeout(() => {
this.currentState = newState;
this.stateEnteredAt = Date.now();
resolve(`Transition from ${this.currentState} to ${newState} completed in ${transitionDuration}s.`);
}, transitionDuration * 1000);
if (signal) {
signal.addEventListener('abort', () => {
clearTimeout(timeoutId);
reject(new Error('Transition aborted'));
});
}
} else {
this.currentState = newState;
this.stateEnteredAt = Date.now();
resolve(`Immediate transition to ${this.currentState} completed.`);
}
});
}
handleRuntimeTracking(newState) {
//Handle runtime tracking based on active states
const wasActive = this.activeStates.has(this.currentState);
const willBeActive = this.activeStates.has(newState);
if (wasActive && !willBeActive && this.runTimeStart) {
// stop runtime timer and accumulate elapsed time
const elapsed = (Date.now() - this.runTimeStart) / 3600000; // hours
this.runTimeHours += elapsed;
this.runTimeStart = null;
this.logger.debug(
`Runtime timer stopped; elapsed=${elapsed.toFixed(
3
)}h, total=${this.runTimeHours.toFixed(3)}h.`
);
} else if (!wasActive && willBeActive && !this.runTimeStart) {
// starting new runtime
this.runTimeStart = Date.now();
this.logger.debug("Runtime timer started.");
}
}
handleMaintenancetimeTracking(newState) {
//is this maintenance time ?
const wasActive = (this.currentState == "maintenance"? true:false);
const willBeActive = ( newState == "maintenance" ? true:false );
if (wasActive && this.maintenanceTimeStart) {
// stop runtime timer and accumulate elapsed time
const elapsed = (Date.now() - this.maintenanceTimeStart) / 3600000; // hours
this.maintenanceTimeHours += elapsed;
this.maintenanceTimeStart = null;
this.logger.debug(
`Maintenance timer stopped; elapsed=${elapsed.toFixed(
3
)}h, total=${this.maintenanceTimeHours.toFixed(3)}h.`
);
} else if (willBeActive && !this.runTimeStart) {
// starting new runtime
this.maintenanceTimeStart = Date.now();
this.logger.debug("Runtime timer started.");
}
}
isValidTransition(newState) {
this.logger.debug(
`Check 1 Transition valid ? From ${
this.currentState
} To ${newState} => ${this.validTransitions[this.currentState]?.has(
newState
)} `
);
this.logger.debug(
`Check 2 Transition valid ? ${
this.currentState
} is not equal to ${newState} => ${this.currentState !== newState}`
);
// check if transition is valid and not the same as before
const valid =
this.validTransitions[this.currentState]?.has(newState) &&
this.currentState !== newState;
//if not valid
if (!valid) {
return false;
} else {
return true;
}
}
getStateDescription(state = this.currentState) {
return this.descriptions[state] || "No description available.";
}
getRunTimeHours() {
// If currently active add the ongoing duration.
let currentElapsed = 0;
if (this.runTimeStart) {
currentElapsed = (Date.now() - this.runTimeStart) / 3600000;
}
return this.runTimeHours + currentElapsed;
}
getMaintenanceTimeHours() {
// If currently active add the ongoing duration.
let currentElapsed = 0;
if (this.maintenanceTimeStart) {
currentElapsed = (Date.now() - this.maintenanceTimeStart) / 3600000;
}
return this.maintenanceTimeHours + currentElapsed;
}
}
module.exports = stateManager;