/** * How long to let the panel sleep. * * In BYOS there is no way to push: the device sleeps, wakes, asks, and sleeps * again. The only lever is how long it sleeps for, so this is the entire * freshness-versus-battery trade-off of the product, in one function. * * Two rules: * - sleep briefly while the shop is trading, and at length overnight; * - never sleep through a change of state. If the door opens in twenty * minutes, wake in twenty-one, whatever the base interval says. */ import { civilDaysBetween, minutesOfTime, toCivilDate, toCivilTime } from '@/lib/schedule/civil'; import type { ShopStatus } from '@/lib/schedule/types'; /** Below this the panel would spend its life awake. */ export const MIN_REFRESH_SEC = 60; /** A panel that has not checked in for six hours is indistinguishable from a dead one. */ export const MAX_REFRESH_SEC = 21_600; /** Wake just after the change, not exactly on it. */ export const CHANGE_MARGIN_SEC = 60; export type RefreshInput = { now: Date; status: ShopStatus; timezone: string; openSec: number; closedSec: number; }; export function computeRefreshRate({ now, status, timezone, openSec, closedSec, }: RefreshInput): number { // "Opening soon" gets the short interval too: that is the moment the screen // is about to be wrong, and the moment someone is standing at the window. const busy = status.status === 'OPEN' || status.status === 'CLOSING_SOON' || status.status === 'OPENING_SOON'; let seconds = busy ? openSec : closedSec; if (status.nextChangeAt) { const untilChange = secondsUntil(now, status.nextChangeAt, timezone); if (untilChange > 0) { seconds = Math.min(seconds, untilChange + CHANGE_MARGIN_SEC); } } return clamp(Math.round(seconds)); } function clamp(seconds: number): number { return Math.min(MAX_REFRESH_SEC, Math.max(MIN_REFRESH_SEC, seconds)); } /** * Seconds from now until a wall-clock moment. * * Measured on the wall clock, which is off by an hour on the two nights a year * the clocks change. That only shifts one wake-up, and the alternative — a * real timezone conversion back to an instant — buys nothing the panel can * perceive. */ function secondsUntil(now: Date, target: { date: string; time: string }, timezone: string): number { const today = toCivilDate(now, timezone); const nowMinutes = minutesOfTime(toCivilTime(now, timezone)); const days = civilDaysBetween(today, target.date); const minutes = days * 1440 + minutesOfTime(target.time) - nowMinutes; return minutes * 60 - now.getUTCSeconds(); }