Summary
A schedule that is continuously active (for example a 12:00 AM → 12:00 AM schedule on all days, which the engine treats as a 24-hour rolling window) on a Nixie-managed circuit causes the circuit to be force-turned-off approximately one minute after the local midnight rollover. The off-event comes from checkCircuitEggTimerExpirationAsync, not from the schedule logic deciding the schedule has expired.
Concrete observation (May 22 2026, user @pawmmm)
May 22 00:00:02 ... warn: dt:Fri May 22 2026 00:00:02 GMT-0600 lat:undefined lon:undefined Not enough information to calculate Heliotrope.
May 22 00:01:02 ... info: NCP: Setting Circuit Pool Filter to false
May 22 00:01:02 ... info: NCP: Setting Pump IntelliFlo VSF to 0 RPM.
May 22 06:37:50 ... info: 192.168.1.135 PUT /state/circuit/setState {"id":6,"state":true}
Schedule: single schedule, all 7 days, startTime 12:00 AM, endTime 12:00 AM. The pump stayed off ~6.5 hours until the user manually re-armed it via dashPanel.
(The Heliotrope warning at 00:00:02 is unrelated — it is logged once when the date setter resets _warningSuppressed on day-change and is only a red herring here.)
Root cause
Two separate but reinforcing bugs in the cstate.endTime refresh path:
Bug A — SystemBoard.syncScheduleStates never refreshes cstate.endTime after the schedule's window advances
controller/boards/SystemBoard.ts lines 3716–3735 (commit 8961410):
public syncScheduleStates() {
try {
ncp.schedules.triggerSchedules();
for (let i = 0; i < state.schedules.length; i++) {
let schedIsOn: boolean;
let ssched = state.schedules.getItemByIndex(i);
let scirc = state.circuits.getInterfaceById(ssched.circuit);
let mOP = sys.board.schedules.manualPriorityActive(ssched);
if (scirc.isOn && !mOP && ssched.scheduleTime.shouldBeOn) schedIsOn = true
else schedIsOn = false;
if (schedIsOn !== ssched.isOn) {
ssched.isOn = schedIsOn;
sys.board.circuits.setEndTime(sys.circuits.getInterfaceById(ssched.circuit), scirc, scirc.isOn, true);
}
ssched.emitEquipmentChange();
}
} catch (err) { logger.error(`Error synchronizing schedule states`); }
}
setEndTime is called only when schedIsOn !== ssched.isOn. For a 24-hour-rolling schedule ssched.isOn is always true, so the transition condition is never satisfied. Meanwhile ssched.scheduleTime.endTime advances at every midnight rollover (per ScheduleTime.calcSchedule — see controller/State.ts:1341), but the circuit-level cstate.endTime stays frozen at the value computed when the circuit was last turned on. After midnight, cstate.endTime is in the past relative to now, and checkCircuitEggTimerExpirationAsync (in controller/nixie/circuits/Circuit.ts:433) fires:
if (typeof cstate.endTime !== 'undefined') {
if (cstate.endTime.toDate() < new Timestamp().toDate()) {
await sys.board.circuits.setCircuitStateAsync(cstate.id, false);
}
}
The 60-second gap between 00:00:02 (heliotrope warning marking the date roll) and 00:01:02 (the off command) matches the stale cstate.endTime being yesterday's 00:00:59.999 and the next 3-second processStatusAsync poll falling after 00:01:00.
Bug B — IntelliCenterBoard.syncScheduleStates has a dead refresh loop
controller/boards/IntelliCenterBoard.ts lines 4810–4820:
for (let i = 0; i < scheds.length; i++) {
let ssched = scheds[i];
if (!ssched.isOn || ssched.disabled || !ssched.isActive) continue;
let c = circs.find(x => x.state.id === ssched.circuit);
if (typeof c === 'undefined') {
let cstate = state.circuits.getInterfaceById(ssched.circuit);
c = { state: cstate, endTime: ssched.scheduleTime.endTime.getTime() };
circs.push; // ← missing function call
}
if (c.endTime < ssched.scheduleTime.endTime.getTime()) c.endTime = ssched.scheduleTime.endTime.getTime();
}
for (let i = 0; i < circs.length; i++) {
let c = circs[i];
if (c.state.endTime.getTime() !== c.endTime) {
c.state.endTime = new Timestamp(new Date(c.endTime));
c.state.emitEquipmentChange();
}
}
circs.push; is a reference to the method, not a call. c is never added to circs, so the second loop iterates over an empty array and never refreshes cstate.endTime. The IntelliCenter board has the right idea about how to fix Bug A but the loop is broken.
Proposed fix
Two changes in one PR:
-
SystemBoard.syncScheduleStates: also refresh cstate.endTime when a schedule is active on an on-circuit and its scheduleTime.endTime has advanced past cstate.endTime.
-
IntelliCenterBoard.syncScheduleStates: fix circs.push; → circs.push(c); so the refresh loop actually runs.
Out of scope (mentioning for awareness, not proposing to fix here)
Once Bug A is fixed, the egg-timer expiration no longer fires spuriously for continuous schedules, and the secondary "stuck ssched.triggered" behavior in controller/nixie/schedules/Schedule.ts:120-188 (where a force-turned-off circuit can't be re-armed by the still-active schedule because triggered was left at true) becomes moot for this scenario. Whether the trigger-loop should also reset triggered on external off-events is a design decision tangled with Manual OP semantics; happy to file a follow-up issue if maintainers want.
Environment
- njspc commit 8961410 (master, 2026-05-15)
- Reported and traced by @pawmmm on a Raspberry Pi running v9.1 against an IntelliFlo VSF + Core55 SWG + Raypak heater stack
Summary
A schedule that is continuously active (for example a
12:00 AM → 12:00 AMschedule on all days, which the engine treats as a 24-hour rolling window) on a Nixie-managed circuit causes the circuit to be force-turned-off approximately one minute after the local midnight rollover. The off-event comes fromcheckCircuitEggTimerExpirationAsync, not from the schedule logic deciding the schedule has expired.Concrete observation (May 22 2026, user @pawmmm)
Schedule: single schedule, all 7 days, startTime 12:00 AM, endTime 12:00 AM. The pump stayed off ~6.5 hours until the user manually re-armed it via dashPanel.
(The Heliotrope warning at 00:00:02 is unrelated — it is logged once when the date setter resets
_warningSuppressedon day-change and is only a red herring here.)Root cause
Two separate but reinforcing bugs in the
cstate.endTimerefresh path:Bug A —
SystemBoard.syncScheduleStatesnever refreshescstate.endTimeafter the schedule's window advancescontroller/boards/SystemBoard.tslines 3716–3735 (commit 8961410):setEndTimeis called only whenschedIsOn !== ssched.isOn. For a 24-hour-rolling schedulessched.isOnis alwaystrue, so the transition condition is never satisfied. Meanwhilessched.scheduleTime.endTimeadvances at every midnight rollover (perScheduleTime.calcSchedule— seecontroller/State.ts:1341), but the circuit-levelcstate.endTimestays frozen at the value computed when the circuit was last turned on. After midnight,cstate.endTimeis in the past relative tonow, andcheckCircuitEggTimerExpirationAsync(incontroller/nixie/circuits/Circuit.ts:433) fires:The 60-second gap between
00:00:02(heliotrope warning marking the date roll) and00:01:02(the off command) matches the stalecstate.endTimebeing yesterday's00:00:59.999and the next 3-secondprocessStatusAsyncpoll falling after00:01:00.Bug B —
IntelliCenterBoard.syncScheduleStateshas a dead refresh loopcontroller/boards/IntelliCenterBoard.tslines 4810–4820:circs.push;is a reference to the method, not a call.cis never added tocircs, so the second loop iterates over an empty array and never refreshescstate.endTime. The IntelliCenter board has the right idea about how to fix Bug A but the loop is broken.Proposed fix
Two changes in one PR:
SystemBoard.syncScheduleStates: also refreshcstate.endTimewhen a schedule is active on an on-circuit and itsscheduleTime.endTimehas advanced pastcstate.endTime.IntelliCenterBoard.syncScheduleStates: fixcircs.push;→circs.push(c);so the refresh loop actually runs.Out of scope (mentioning for awareness, not proposing to fix here)
Once Bug A is fixed, the egg-timer expiration no longer fires spuriously for continuous schedules, and the secondary "stuck
ssched.triggered" behavior incontroller/nixie/schedules/Schedule.ts:120-188(where a force-turned-off circuit can't be re-armed by the still-active schedule becausetriggeredwas left attrue) becomes moot for this scenario. Whether the trigger-loop should also resettriggeredon external off-events is a design decision tangled with Manual OP semantics; happy to file a follow-up issue if maintainers want.Environment