Day/Night¶
src/daynight.c implements native, automatic day/night switching,
replacing thingino's separate daynightd daemon. Built with
USE_DAYNIGHT (on by default — see Building); without it,
stub functions report enabled=0 and fall back to whatever
image.running_mode currently holds.
Since v1.10.1 the decision engine is new (
dev_notes/dn-redesign.md,docs/ai/config-keys.md§14). Day→night: the day pipeline abovenight_gainor the AE at its limit forday_confirm_s(6 s when very dark). Night: a look (IR illuminator off 8 s, silent) every 45 min or when the lit level fallsjump_pct(20 %) below the last look; a check (day pipeline, one click) when the look reads below the bar; failed checks lock out for 1, 2, 4 … min; a check anyway afterheartbeat_max_s(18 h). Since v1.11.0 a look also starts when the colour of the lit picture (AWB statistics) movescolor_pct(3 %) from the last look for 20 s — a weak lamp that barely brightens the IR picture (at most every 5 min, doubling to 45 min; statuscolor_trigger,color_dev_pct); a look taken while the lit level is still moving sets no reference, the next settled level does; vendor T21 no longer readsisp_path. Since v1.12.0 (dawn): a look whose lit level already sits at the bar (lit × 0.9 ≥ bar) is skipped, the illuminator stays on; a creeping light (no 10 s step of more than 15 %) is judged with the jump bar and a failed check'sk; near the bar a creep looks once the last dark reading predicts day (look (near)); a lamp lit right after a check is not taken as the night reference. Retired:probe_min_gap_s,probe_confirm_s,boot_probe,diagnose_thresholds, the IR-ratio verdict, the trend trigger, the night reference ratchet and its vetoes. The sections below that describe probes, the ratio and the heartbeat describe v1.10.0 and earlier; Idle-Dark,switch_cmd, the hooks and the calendar still apply.
What timps does — and does not — control directly¶
A single background thread (dn_thread) decides day vs. night and, on a
change, runs an external board switch script as
<daynight.switch_cmd> day|night (default script name daynight, via
fork()+execlp() — never system(), so a config value can't inject
shell commands). timps does not flip the ISP mode itself from this
path — the board script is expected to drive the physical IR-cut
filter and IR LEDs, and then call back into timps's own /control to set
image.running_mode, closing the loop:
flowchart TB
a["dn_thread decides day/night"] --> b["vfork()+execlp(switch_cmd, day | night)<br/>e.g. thingino's daynight script"]
b --> c["board script flips the IR-cut filter and the IR LEDs, then"]
c --> d["POST /control #123;#quot;image#quot;:#123;#quot;running_mode#quot;:0 | 1#125;#125;<br/>(the colour hook)"]
d --> e["control.c: timps_apply_setting(#quot;image.running_mode#quot;, ...)"]
e --> f["hal_ingenic.c: IMP_ISP_Tuning_SetISPRunningMode(...)"]
f --> g["fs_kick_chn0() (see below)"]
This indirection keeps the whole board in sync: the same script that
already knows how to drive this specific camera's IR-cut hardware is
responsible for telling the ISP the new mode, rather than timps
duplicating board-specific logic. The mode change is committed
(cur=target) even if the script exits non-zero (logged as a warning) —
a missing script only warns once, it doesn't retry every sample.
The one asymmetry everything follows from¶
The two optical paths are not equally trustworthy, and every rule below is a consequence of that:
- Day pipeline — IR-cut closed, illuminator off, colour tuning. The exposure the ISP settles on is an honest measure of ambient light.
- Night pipeline — IR-cut open, illuminator on. The camera is partly measuring its own light. An absolute reading here means nothing; only a change in it means anything.
So day→night is a plain measurement, and night→day can only be answered by
physically switching to the day pipeline and looking — a probe, which
costs an audible IR-cut click. All of the scheduling in daynight.c exists
to bound how often that click happens without ever giving up the ability to
correct a wrong night.
Design note. Between 2026-08-02 and 2026-08-16 this file accumulated nine separate rules rationing that one probe — backoff, failure ratchet, anchor override, evidence skip,
probe_max_skip_s, trend suspension, brightening hold, oscillation breaker, verify deadlines. Each was correct and each was bought with a real incident, but because they all gated the same single path their failures multiplied instead of cancelling; the worst case was a camera rendering IR video in broad daylight for four hours while a service restart fixed it in ten seconds. The 2026-08-17 redesign replaces them with independent paths (five today) and one rate limit. The full argument is indev_notes/DAYNIGHT_REDESIGN_2026-08-17.md; the incident record it came from is in Day/Night Design Notes.
The metric: exposure index, not gain¶
D = total_gain × (integration_time / max_integration_time) higher = darker
total_gain alone has a hard floor at 256 (1.0×). Once the AE rails
there, further brightening of the scene is invisible — which is why a
camera resting at 256–268 under its own IR (mounted close to a reflective
object) could never have its brightening detected at all, no matter how the
probe was scheduled. The AE has a second control, and when the gain bottoms
out it keeps shortening the exposure instead, so the product carries the
signal across the whole range.
- Dark scene: integration railed at max →
D == total_gain. Identical to the pre-redesign behaviour, and the reasonday_gain/night_gainkeep their historic calibration. - Bright scene:
Dfalls far below 256, continuously. max_integration_timeunreadable →D = total_gain, i.e. clean degradation to the old behaviour.- Normalising on
maxmakes it frame-rate independent.
Both fields come from the daynight.isp_path scrape, so that scrape now runs
on every decision tick — which is what the 2 s default interval_ms pays for
(the gain half still prefers IMP_ISP_Tuning_GetTotalGain where available).
Since v1.11.0 vendor T21 skips the scrape (the vendor tx-isp-t21 driver
corrupts the kernel heap on that proc read; an oops killed the day/night
thread) and takes the integration-time ratio from IMP_ISP_Tuning_GetExpr.
Five paths, none of which can block another¶
| Path | Signal | Covers | Cost | |
|---|---|---|---|---|
| A | day → night | day-pipeline D vs night_gain, held day_confirm_s |
dusk, lights off | 1 click |
| C | spontaneous probe | relative fall of D below probe_jump_pct% of the night reference, held probe_confirm_s |
lights on; indoor cameras; cameras with no location data — the path that carries most of a fleet | 2 clicks if it fails |
| T | trend probe | a 3-minute EMA of D below 75% of a 60-minute one, held probe_confirm_s |
dawn — a ramp too slow for path C's step bar (measured: a real twilight needs 106 minutes to reach it) | 0 clicks; only armed when daynight.irprobe_cmd makes the probe silent |
| B | heartbeat probe | wall clock only, no sensor | a reference anchored too low, a scene the index cannot read | 2 clicks |
| D | boot | one measurement at t=0, then one assertion |
the persisted mode is a guess of unbounded age, and the board may be in the other mode | 1 switch_cmd if the answer is day, 2 if night |
When night_gain is out of reach (path A)¶
Path A needs an index above night_gain, and the index is gain × ratio with
the ratio at most 1. An AE that stops at its analog cap and leaves the digital
gain at 0 therefore tops out at the cap itself, 256·2^(MAX SENSOR analog
gain/32) — 4096 for a cap of 128, exactly the default night_gain. Measured
on one Wyze Pan v1 (T20/jxf22, OpenIMP), one night: analog 128/128, ISP
digital 0 of 32, integration time 87.6 %, d = 3587 on almost every sample —
the camera stayed in day in a pitch-dark cellar, and the 32 unused digital
units counted as reserve, so the railed variant of path A did not fire
either.
Since v1.10.0, where the published cap is <= night_gain
(provably out of reach while the digital gain stays unused), path A also
counts a day tick as dark when the analog gain is at >= 98 % of its cap, the
ISP and sensor digital gain read 0 and the smoothed index is within 95 % of
its highest settled value of that stretch at the cap — held day_confirm_s
like the threshold. The plateau bar must clear 1.5 × day_gain, or the next
probe would read the same scene as day and undo it. One WARN per session names
the numbers. After the first such switch every night_gain comparison in
the loop (the plain day test, the filter-cost and noir vetoes) uses that bar,
so a ratio verdict cannot chase the rule. Night→day and boot are untouched:
both read against day_gain, which a dark room exceeds. Everywhere the cap is
above night_gain, or the dump has no cap or no ISP digital gain line,
nothing changes. The pure rule is src/dn_reach.h (make test-dn-reach);
corpus scenarios 55–59 (59: a lamp makes the silent probe say day while the
day pipeline stays at the cap — one learning round trip, then the filter-cost
veto holds night).
The plateau is the highest settled value of the current stretch at the cap,
not a learnt constant, so one transient cannot raise it for good. The price:
on an AE that reaches the cap before its integration-time limit, the switch
comes once the cap has been held day_confirm_s with the index not falling
more than 5 %, possibly before the integration time has topped out (bounded
below only by the 1.5 × day_gain bar).
What is measured: one camera, one night, the stuck-in-day symptom and its
numbers. What is not: the rule on that camera (pending a field test), why that
AE stops where it does (OpenIMP), and any camera whose cap is slightly above
night_gain but whose AE stops below it — the cap proves nothing there and
the old behaviour stays.
That last case turned out to be the Pan itself. On the open stack the dump
is isp-m0, written by open-tx-isp's compact T20 AE, and in that mode the
MAX SENSOR analog gain line prints the sensor maximum (attr->max_again >>
11, 158 on jxf22) while the AE clamps at its tuning ceiling (128). Derived,
not read off the camera: the open-tx-isp 3cf9bdac source of the image, and the
probe line's lit_hr=124, which is (158 − 66) + 32 for the analog 66 the
exposure log shows at that moment (128 would give 94). With 158 the cap is
7838 > 4096, so the plateau rule never fired in the 2026-10-07 soak (no
reachable plateau line in the central log).
Confirmed on the device 2026-10-09 (Pan, open-tx-isp 3cf9bdac, timps
ed60eae): isp_info on the same camera publishes MAX SENSOR analog gain :
128 — the AE's cap — next to isp-m0's 158. Reproduced with
daynight.boot_probe = 0 and a persisted day (image.running_mode = 0) in
the dark: day adopted at boot, d 3587 constant, no switch. Since
v1.10.0 timps reads that line from isp_info beside a configured isp-m0
(cached, re-read every ~5 min) and uses the lower of the two maxima for the
plateau rule and the AE reserve; it logs once … publishes MAX SENSOR analog
gain 158, isp_info 128: using 128 …. On the Pan the reserve at the cap drops
from 30 + 32 to 0 + 32 units (the IT stop), still above the 8-unit clip
limit, so nothing else changes there. Scenario 64 models the reproduction
(one switch to night after day_confirm_s), 65 is its negative control
(isp_info 160: the lower value stays 158, day as before); not yet run on
the camera. On older builds: daynight.night_gain below the plateau, e.g.
3000.
When the silent probe sees light the day pipeline does not¶
A silent probe compares the night pipeline (IR-cut open) with and without the camera's own illuminator. r ≤ 2 means "the illuminator is not what lights this scene" — which is also true when the light is infrared from somewhere else: another camera's illuminator in the same cellar. The IR-cut filter removes that light, so the day pipeline stays dark. Two cameras, one night each (2026-10-07):
- Wyze Pan v1 (T20/jxf22, OpenIMP): came up in day (
running_mode=0), the boot measurement read the day pipeline at 3587 and switched to night. 20 s later path C fired against a reference of 3489 — the level it had come up with, i.e. a day-pipeline level — the probe read lit 401, dark 483, r = 1.20, and switched to day. The plateau rule (above) was inert, so the camera stayed in day: no DAYNIGHT line for the rest of the night. - SecuPlug SP1 (T10L/jxh42, OpenIMP):
switching to day (IR ratio 1.36)at 23:17:38,switching to night (exposure 4179 > 4096)at 23:18:36 — a switch pair per such probe. (From the field notes; this camera is not in the central syslog, so its probe line is not quoted here.)
Since v1.10.0 two changes:
- A night that a measurement proved — path A's switch, a probe that found
night (the boot measurement included), a silent probe with r ≥ 2, the
pegged or filter-cost verdict — keeps a flag. While it is set, a silent
probe's "the room supplies the light" only switches to day if the lit
level is measurably brighter than the night reference (below 90 % of it,
the heartbeat's
DN_MOVED_MARGIN). Otherwise it stays night — light the day pipeline does not see:silent probe (…): r=… but lit … is no brighter than the proven night reference …. A night that was adopted (another tool switched the ISP,boot_probe=0) is not proven and keeps the old behaviour, so a night adopted in daylight still ends at the next probe. The filter-cost veto runs first and is unchanged. - When the boot measurement starts from a board that came up in day, its pre-level is not used as the night reference (it is the day pipeline's); the reference is anchored from the night pipeline after 30 s, like after any other switch. That removes the immediate path-C probe after such a boot.
Unchanged: a real light (the lit level falls below 90 % of the reference)
goes to the ratio exactly as before, and so does a dawn — r can only fall
below 2 once the ambient light outweighs the illuminator, which puts the lit
level under the reference. Residual: a reference anchored before the other
cameras' illuminators came on is darker than the scene with them, so the
first probe after that can still try day once; the bounce off night_gain
re-anchors the reference and the next probe is held. On the Pan, a legitimate
day (cellar lamp on) that ends with the lamp off still has no way back to
night while timps reads only isp-m0's 158 (fixed by the isp_info read
above). The pure gate is
src/dn_proven.h (make test-dn-proven); corpus scenarios 60 (Pan), 61
(T10), 62 (a lamp still switches to day), 63 (a dawn after a night of held
probes switches once).
The construction rule, and the thing the previous design lacked:
A rationing rule may only be applied while the independent trigger it defers to is actually working.
That is not an aspiration but a runtime predicate (dn_c_sighted()): if the
integration-time fields are unavailable and the night reference sits at the
gain floor, path C is structurally blind, so the heartbeat is not allowed to
stretch itself on path C's behalf.
The night reference¶
One number, and it is a proof, not a filter state:
ref= the exposure level at which night was last proven — either by entering night from day, or by a probe that went and found darkness.
A probe that fails is simultaneously a proof of night and a contemporaneous
night-pipeline reading, which is why one variable does the job the old design
spread across night_baseline (drifting), smooth_tg, probe_fail_smooth,
min_smooth_since_probe and brighten_ref. It never drifts. It moves in
exactly two places, and the failed-probe assignment is the entire "ratchet":
after a failed probe the bar is derived from the level that did fire, so a
repeat of the same stimulus (headlights, a dip in an overcast dawn) cannot
re-fire it.
Nothing is load-bearing on any single sample the way night_baseline was:
- reference anchored too high → one probe fires, finds night, and
re-anchors at the true resting level. Permanently correct after one
self-correcting click pair. (The
a5dae07incident was this exact situation looping forever every 25 minutes.) - reference anchored too low → path C goes quiet until the heartbeat
re-anchors it. Bounded by
heartbeat_max_s.
It can also be lowered directly, with no probe needed: a silent probe
verdict of "night" — the illuminator carries the scene (r >=
ir_ratio_night), or day mode is measured-to-bounce (the filter-cost
projection; that branch forgot the rule until 2026-08-21 and cam-06
re-fired every 26 s against a stale bar, scenario
26-projection-verdict-no-way-down) — at a level below the current
probe bar (ref * probe_jump_pct / 100) is proof in the other direction —
the reference predicted a brightening worth looking at, and the look found
darkness, so the reference described a scene that no longer exists. Without
this a reference anchored too high while the ISP was misbehaving had no way
down at all: measured on a camera whose ISP came up wrong and anchored at
131072, then read 6070 once it recovered — the jump trigger fired every
fourteen seconds, the silent probe correctly answered "the illuminator
carries this scene," and nothing moved. Seventeen probes in a row, which
reads from outside as an IR lamp that keeps switching itself off. Logged as
night reference lowered to <level>, proven by the silent probe (bar
<level>). Corpus scenario 23-stale-reference-no-way-down.
Since 2026-08-21 the reference additionally refuses clips: a reading
taken while the AE is railed (reserve known and < ir_min_headroom) is a
clip, not a level — it may prove night, but it may not be remembered. The
post-entry anchor waits for an honest sample (night reference deferred: …),
and a failed probe whose pre-probe level was a clip leaves the reference
unset instead of ratcheting the clip in. Until a trustworthy reading arrives
the automaton runs on the configured absolute thresholds and the heartbeat
alone, which changes nothing about its steady-state probe cadence. The same
rule keeps clips out of filter_cost, the threshold diagnostic and the
trend memory. Measured: a T23 whose ISP boots railed anchored 131072 where
the scene was worth a twentieth of that, and the probe bar of 65536 then sat
above anything a real brightening could reach. Corpus scenario
24-pegged-boot-poisoned-reference.
The trend (path T)¶
ref answers a step; it cannot answer a ramp. The bar sits at
probe_jump_pct (50%) of a proven night level, and natural twilight takes an
hour or more to get there — measured on cam-C, the dawn of
2026-08-18 fell 11839 → 5312 over two hours (a factor of 2.23) and did not
reach the bar until 07:31, 67 minutes after sunrise. Meanwhile the camera
renders IR video in daylight.
So night also keeps two EMAs of the index and compares them with each other:
fast = EMA, tau 3 min (follows the scene)
slow = EMA, tau 60 min (remembers it)
probe when fast / slow < 75%, held probe_confirm_s
Nothing absolute is consulted — the question is "is this scene brighter than it remembers being", which is the one relative question the night pipeline can answer honestly. Both EMAs are re-anchored after every verdict, so an answered question cannot immediately re-ask itself, and both are frozen while the illuminator is off (that is a third optical state, not a darker version of this one).
Measured over 12 cameras and 181 camera-hours of night
(scripts/dn-trend-eval.py): 10 of 12 dawns found, 0.22 false fires per
camera-hour at these constants. Shorter memories fire later and find
fewer, because they track the twilight instead of noticing it.
Only armed when
daynight.irprobe_cmdis set. That rate is affordable as silent probes — a few seconds of dimmer image each — and not as audible ones: it would be roughly 2.6 extra motor movements per 12-hour night. A board that cannot switch its LEDs separately keeps path C plus the heartbeat.
The constants are compile-time (DN_TREND_FAST_MS, DN_TREND_SLOW_MS,
DN_TREND_PCT in src/daynight.c), not config keys: they are dimensionless
and fleet-wide by construction, which is the same property that made the IR
ratio worth having in the first place.
The silent probe¶
Where the board can drive its IR illuminator without moving the IR-cut filter
(daynight.irprobe_cmd), every request for a verdict tries this first:
turn the illuminator OFF
wait probe_settle_s for the AE
r = index(illuminator off) / index(illuminator on)
turn the illuminator back ON
r is a ratio, so it is dimensionless and the same pair of thresholds fits
every camera in a fleet. No absolute level does: genuine daylight readings
span a factor of 63 across twelve cameras at one instant.
| verdict | condition | costs |
|---|---|---|
| night | lit reserve < ir_min_headroom |
nothing — r divides two clips and says nothing; railed-dark is still night evidence |
| night | r >= ir_ratio_night |
nothing — the illuminator was carrying the scene |
| night | reserve < ir_min_headroom |
nothing — the meter is pegged at the dark end, which is itself proof |
| escalate | reserve unknown (no ceiling fields in the ISP dump) | one audible probe — a lit room and a railed meter look identical without the reserve, so the day pipeline judges (2026-08-21; previously this fell into the "pegged" row and answered night forever) |
| day | r <= ir_ratio_day, reserve sufficient, and the filter-cost gate below |
one switch |
| escalate | anything in between | one audible probe |
The headroom test is not a refinement. An AE with nothing left cannot respond
to the illuminator going off, so it returns r ≈ 1 — indistinguishable from
daylight. Measured on a pitch-dark scene: r = 1.14, below ir_ratio_day.
Only the reserve separates that from a genuinely lit room.
A camera whose ISP dump has no ceiling fields (MAX SENSOR analog gain /
MAX ISP digital gain) has no reserve at all — and therefore none of the
clip protection either. That is announced once per session as a WARN
(no gain ceilings in the ISP dump - AE reserve is unknown …), not left to be inferred from
hr=-1 in a probe line. Both fleet T20s do publish the ceilings (jxf23:
32 units ISP digital, jxf22: 45 — same SoC, different sensors, so a
hard-coded value would be wrong on one of them). The open-tx-isp T41 dump is
such a variant: its AeMaxAGain is the uncapped sensor maximum, not the AE's
ceiling, so it is not used. On T41 with the open stack the reserve comes, since
v1.10.0, from IMP_ISP_Tuning_GetAeExprInfo instead (AeMaxAGain,
which open-tx-isp fills with the smaller of sensor maximum and gain cap, against
TotalGainDb, log2 in Q16); integration time adds a stop only while the gain
is still 1x, because above that the AE sits at its own limit, which can be
below the published maximum (T55A dump: 1750 of 2092 lines at 6.9x).
Both ratio thresholds ship at 2.0, so with the defaults the "escalate" row
never fires. Setting ir_ratio_day below ir_ratio_night opens a deliberate
dead zone, trading an occasional audible click for not deciding on thin
evidence.
When the illuminator command doesn't work¶
A board that cannot switch its IR LEDs separately from the filter answers
every attempt to run irprobe_cmd with a failure. Falling back to the
audible probe on each of those tries would be worse than never having
tried it — a motor movement paid for a command that was never going to
succeed. Two consecutive failures retire the silent probe for the
session, and the trend trigger (path T, which only exists to make silent
probing cheap) retires with it, leaving the jump trigger (path C) and the
heartbeat — the fleet's pre-existing behaviour on a board without a working
irprobe_cmd. Not persisted: a restart re-tests it, which is the cheap
direction to be wrong in. Logged once, at the second failure: '<cmd>'
failed N times - silent probe retired until restart; the trend trigger goes
with it, leaving the jump trigger and the heartbeat.
POST /control {"daynight":{"probe":1}} asks for one silent probe on the
next tick — useful to confirm a camera can see daylight without waiting for
the jump trigger, the trend or the heartbeat. It is rejected (not silently
swallowed) when there is no irprobe_cmd configured or the probe has
retired itself as above; see HTTP /control API Reference.
What switching to day also costs¶
A low ratio answers is the illuminator earning its keep. It does not answer is there enough light to run day mode — because switching to day also closes the IR-cut filter, and in a dim interior that filter alone can cost a factor of three.
Left unguarded the two rules chase each other. Measured on a bedroom-class
scene with one dimmed lamp: the probe reports r = 1.27 and switches to day;
the day pipeline, filter now closed, reads 11480 against a night_gain of
4096; path A sends it straight back; the illuminator returns and the ratio
reports 1.27 again. Eight round trips in one evening, each an audible click.
So the automaton measures filter_cost — the day reading over the night
reading the verdict left, 3.27 in that scene — the first time a ratio
verdict is undone that way, and from then on requires
current night reading × filter_cost < night_gain
before it will act on a ratio at all. One exploratory switch is unavoidable:
nothing can know the day-pipeline reading without trying it once. Every later
pass refuses in the log (at LOGD, like every per-probe line — see
Logging) and costs nothing:
silent probe (trend): r=1.27, but day mode would read ~11500 (3.27x) > night_gain 4096
filter_cost may be below 1. The first version of this guard only
learned when the day reading came back higher than the night one, on the
assumption that closing the IR-cut filter can only cost light. A T20 whose
illuminator contributes nothing measurable disproved that: the silent probe
returned r = 1.00 with ample AE reserve, the day pipeline read 6166
against a night level of 8171 — lower, not higher — and 6166 was still
above night_gain, so the guard never armed and the camera flapped 11
times in a day. What filter_cost captures is not "what the filter costs"
but what day mode actually reads in this scene, and that is meaningful in
both directions; the log line where it's first measured says so:
day mode reads 0.75x the night level here (6166 vs 8171) - a ratio verdict
now needs the night reading below 5428
At real dawn the night reading falls, the product drops below night_gain on
its own, and the switch happens — no special case, no calendar. filter_cost
is measured per scene rather than configured, and is deliberately not
persisted: a quantity that moves with the scene is cheaper to re-measure once
per restart than to keep correct in flash. Corpus scenarios
21-ir-ratio-flap-cam-05 (factor above 1) and 22-ir-ratio-flap-t20 (factor
below 1) hold this to two switches where the unguarded automaton spent
twelve.
The probe¶
switch to the day pipeline
wait probe_settle_s for the AE
judge ONCE against day_gain: below → stay (day confirmed)
else → revert immediately, ref := pre-probe level
There is no third, "ambiguous" outcome — which is what the entire
dead-zone/dn_verify/deadline-extension machinery used to exist for. The
revert bypasses transition_s: retreating from a wrong probe is free.
probe_min_gap_s is the only rule rationing probes, so the worst-case
click rate is a property of the configuration rather than of interacting
heuristics. This is also why there is no longer an oscillation breaker: an
IR-reflection feedback loop needs a threshold crossing to flip night→day,
and no threshold can do that any more.
The heartbeat¶
The only bound on how long a wrong night can last, and deliberately a flat
interval rather than a multiplying backoff — a backoff is precisely how the
previous design turned a bounded guarantee (night_reconfirm_s) into an
unbounded one (×4, plus a skip gate, plus a 12 h outer bound).
- scene moving since the last probe →
heartbeat_s(4 h) - scene flat (smoothed range within 15%) and path C sighted →
deferred, but never past
heartbeat_max_s(12 h) since the last probe - a configured calendar can only pull the deadline in, to the next
sunrise — never push it out. Since v1.9.28 that pull-in
applies at every re-arm of the heartbeat; before, it ran at only one of the
re-arm sites, so after the first re-arm of a night the calendar was not
consulted again. The calendar's sunrise here includes
daynight.sun_sunrise_offset_min. Inmode=schedule, where the calendar decides outright, a sunrise/sunset offset that pushes an edge across solar midnight is now honoured too (it used to be clipped to the current solar day, whilesun_computed_*showed the shifted time). A time window counts only when both edges are validHH:MMand differ; anything else is ignored with a startup warning.
A dark closet therefore costs two click pairs a day, and a camera that sees anything at all costs six. Note what the calendar is not doing: it never decides. That is what keeps basements, windowless rooms and artificially lit spaces working, and it is why a camera with no location data loses only sharpness of scheduling, not correctness.
Boot¶
The persisted image.running_mode is a guess about a scene nobody measured,
and nothing bounds how old it is — a camera that spent a year in a drawer
boots carrying a year-old opinion about the light. So since 2026-08-22 boot
does not adopt it. It measures, then asserts:
- Wait for the AE to converge (
DN_BOOT_SETTLE_S, thenDN_STABLE_Non-EMA samples, hard-capped atDN_STABLE_MAX_MS). - Run one ordinary probe into the day pipeline — the same
dn_switch, the sameDN_PROBE_SETTLE_S, the same verdict and the same ISP readback gate a runtime probe uses. Day is the only optical state whose reading means something absolute (IR-cut closed, illuminator off). - Read it against
day_gainand assert the answer on the board once.
Cost, counted honestly: one switch_cmd invocation when the answer is
day — the probe's own drive is the assertion — and two when it is
night. On a board that comes up in its reset state (/run/thingino/
daynight_mode absent, IR-cut in the day position, LEDs off) the first of
those moves nothing, and the second is the movement the camera actually
needs.
That second call is what a dark-time reboot used to be missing: on
2026-08-22 five fleet cameras rebooted after dark, adopted night in
software, and spent the night with the IR LEDs off, because the old boot
path deliberately never called switch_cmd at all. Under this sequence
that outcome is unreachable by construction — every boot ends in exactly one
assertion of the mode it decided.
It also makes restart-equivalence — the property the design notes
identified as the one every stuck-mode incident violated ("a service restart
fixes it") — literal at t=0 for every boot, not only for a persisted
night with boot_probe left on.
The boot probe never takes the silent route, cheap as it is. The silent
probe divides a reading taken with our illuminator on by one taken with it
off, and at boot nothing has established that it was ever on: a board in its
reset state has the LEDs off, so the "lit" reading is not lit, r comes back
near 1, and the verdict is whatever the AE reserve happens to say. In a dark
room that lands on "pegged, therefore night" — the right mode, no
assertion, all night. That is exactly how the desync above went unnoticed.
The boot probe also does not arm the repeating post-switch re-assert. The
re-assert re-drives whatever image.running_mode currently says, and at boot
that is still the persisted guess (only the board hook chain ever writes it,
and it has just been asked to). Armed here it lands DN_REASSERT_MS in —
right where the boot verdict is read — and pushes the stale value over the
optics the probe just commanded. That is the incident a living room already
paid for: the switch to day was overwritten twice, eight seconds apart, and
the room stayed in night mode through daylight. The boot decision's own
switch arms its own re-assert normally.
What the boot probe does do like any other probe is keep its pre-probe
level as the night-reference candidate, and charge probe_min_gap_s. Both
were tried the other way and the corpus refused them. Deferring the anchor to
the ordinary DN_REF_DELAY_S window lets it land inside a lighting
transition — scenario 02's dip to 1200 against a resting 3500 anchors a bar
of 600 that a genuinely lit scene at 900 can never clear, i.e. incident
f8a7b21. Exempting the boot probe from the ration lets that same transient
fire a second probe 24 s later, whose revert re-anchors on the dip: the same
incident through a different door. A reference that lands too high costs
one self-correcting probe; one that lands too low costs the trigger outright.
boot_probe=0 opts out of the measurement, not the assertion. The
persisted value is taken on trust, as it always was under that setting, and
then asserted on the board once. A railed AE (0 units of reserve)
overrides the opt-out and measures anyway: a pegged meter reads a clip rather
than a level, re-asserting running_mode is a documented no-op on an ISP
that came up mistuned, and only a real transition re-tunes it (T23, measured
2026-08-21: day and back read a factor 23 lower within 12 s of each leg).
With boot_probe=1 the measurement is that transition, so the case is
subsumed rather than special-cased.
Fallback. If no usable exposure reading has appeared within
DN_STABLE_MAX_MS of start-up (no ISP file, a wedged /proc dump), there is
nothing to measure. Boot then falls back to the persisted value — and still
asserts it on the board once — with a WRN that says so (boot: no
exposure reading <N>s after start (<path>) - adopting persisted <mode>). Silently behaving
like a normal boot is how a stale value acquires authority it never earned.
Learning (removed 2026-08-22)¶
An earlier version of this automaton recorded each confirmed day's lowest
D and, with daynight.learn=1, let the median of the last 8 raise the
effective day_gain when the configured value turned out to be unreachable
— the failure that had three cameras stuck in night on 2026-08-16. In
practice its own safety clamp (never raise the threshold past night_gain/2)
could not raise it far enough for the cameras that actually needed it, so the
config surface (daynight.learn, daynight.state_path) was removed rather
than kept as dead weight. daynight.diagnose_thresholds covers the same
failure mode today: it names the value to raise day_gain above instead of
trying to raise it automatically.
Changes in v1.12.0¶
- Looks ask only near day. Whatever the bar, a look asks for a check only
when
k × darkis within 4 ×day_gain. After a railed check the bar was0.85 × dark, which says nothing about the distance to day: at dawn the colour trigger asked at 19 ×day_gainand each look cost a click pair. - A darker but recoloured scene looks. When the lit night picture gets
darker while its colour moves beyond
color_pct, timps starts a colour look instead of adopting the darker scene. A lamp switched on while other cameras' infrared in the room went off left a camera in night until the 6 h timer. heartbeat_max_s18 h (was 12 h): a winter night is longer than 12 h, so the guarantee check failed every morning before dawn on every camera.- Looks that cannot find day are skipped. Without the illuminator the
scene reads no brighter, so when the lit level × 0.9 is already at or above
the bar the look is skipped: the illuminator stays on, the lit level
becomes the reference and the timer starts over. Log (debug):
look (<why>) skipped: lit L, bar B. Timer and requested looks are never skipped, nor a look that only learns the bar (after a check without a look of its own, such as the boot or guarantee check). - Creep or lamp from a 10 s step. A lamp steps the lit level by more than
15 % from one 10 s sample to the next (within the last minute); otherwise
the light creeps (dawn, a fire). A creeping look, also a colour look while
the lit level is below the reference (brighter), is judged with the jump
bar and, within 1 h of a failed check, with
day_gain / kof that check. Before, only a creeping jump got the failed check'sk, and colour looks usedday_gain. - Near-bar look. A creep close to the bar moves less than
jump_pctbefore the dark reading would beat it. When the last look's dark reading scaled by the lit level now (dark × lit / reference) predicts a dark reading under 0.95 × the bar, a look runs after the usual 20 s hold (look (near)). cam-wohn lost 60 min of day to this on 2026-11-07. - No lamp as the reference after a check. When the night reference is
taken from the next settled level after a check (also at boot, and through
the look that learns the check's bar) and that level is
jump_pctbrighter than the lit level before the check, it is not adopted: the earlier level stays the reference, so the lamp is a jump and looks. - A near miss waits for the AE. A check that reads under 1.5 ×
day_gainwaits the 10 s day hold before it is called night (the AE is still settling); before, only a day reading waited. - A railed check sets no
k. A check with the AE at its limit or above 4 ×night_gainsays nothing aboutkand no longer sets the failed check'skfor the next hour. - Measured: replay corpus 77 pairs, 0 failed (six new families and profiles from the measured 2026-11-07 dawns of cam-wohn and cam-schuppen); a fleet what-if over 121 mornings: failed morning checks 178 → 94, day on average 5 min later, about half of the morning looks skipped. Background: Day-Night Design Notes.
Changes in v1.11.0¶
- Jump bar from what failed. Every look → check pair teaches
k(day reading / dark look reading); the dark readings of the last failed checks are kept across jumps. A jump look asks again only below 0.9 × the lowest failed dark reading (at most 2 ×day_gain, at leastday_gain / k). - Light that creeps in (dawn with daylight infrared, k 4-5; a fire) asks
again within 60 min of a failed check only once that check's
kpredicts day; a sudden light (a lamp: the level moved more than 15 % within the minute before the jump) still asks at once when it pulls the dark reading below 70 % of the failed one. Measured: a bedroom dawn went from 6 to 3 failed checks. (v1.12.0 tells creep from lamp by a 10 s step instead, see above.) - A check's "day" must hold. "Night" is final at once; "day" only once the
reading has stayed below
day_gainfor 10 s after the settle time. The open T40 driver reported the night-side exposure for about 14 s after the switch, so a dark check read "day" and the camera sat in day mode with its illuminator off, setting off every IR camera next to it. - Colour trigger (
daynight.color_pct): a colour change of the lit picture starts a look; with it working, the timer look runs every 6 h. - Boot check at once when clearly dark, otherwise after at most 30 s.
- A keyframe 0.8 s after every IR or IR-cut switch; day/night is held in day while an OpenIMP test pattern runs.
Anti-flapping guards¶
Three, all unchanged from the pre-redesign machinery, which the design notes correctly identified as the part that was working:
transition_s— minimum dwell between switches. A failed probe's revert bypasses it.- Confirmation windows —
day_confirm_sfor path A,probe_confirm_sfor path C. Also keeps theSetISPRunningModecall from landing mid AE ramp, which is the stuck-mode class the latch section below covers. - Post-switch re-assert —
DN_REASSERT_MS/DN_REASSERT_COUNT, plus a warning whenrunning_modenever follows a switch (a lost POST from the fire-and-forget board hook chain, or a manual override).
Regression corpus¶
scripts/dn-replay.py --all scripts/dn-scenarios drives a host-built
timpsd-sim through synthetic two-pipeline traces on a virtual clock. Fifteen
scenarios, one per historical incident plus the two the redesign added, each
asserting a mode timeline, a click budget, a maximum continuous wrong-mode
run, restart-equivalence and probe-time monotonicity.
Two of them exist as a pair and should stay that way:
- 09-dawn-ramp feeds no integration-time channel, so the exposure index
degrades to bare gain and its night curve floors at 260 — a sensor pinned at
the gain floor. Path C is structurally exhausted exactly as dawn completes,
and recovery costs one
heartbeat_s. That is the honest bound of the degraded path. - 15-dawn-ramp-exposure-index is the same dawn with the integration-time channel. The scene keeps producing evidence, path C fires on its own, and day is confirmed within minutes — with no extra rule, timer or state.
The difference between the two is the entire argument for changing the metric, written as an assertion rather than a claim.
The ISP running-mode latch quirk and fs_kick_chn0()¶
Even with all of the above working correctly, one more hardware quirk
remained and was root-caused on a real deployed camera (cam-L Y4,
T23/sc2336) on 2026-08-01: the ISP core only processes a queued
SetISPRunningMode change while FrameSource channel 0 is actively
delivering frames.
With chn0 idle-stopped — the normal state when nobody is watching the
main stream, per timps's on-demand encoding
design — IMP_ISP_Tuning_SetISPRunningMode() still returns success, but
the driver silently leaves the change queued indefinitely. This happens
even while a different, always-scaled substream channel (chn1) keeps
streaming the whole time: on the camera where this was found, the dusk
day→night switch (plus both automatic re-asserts) all reported success,
/proc/jz/isp/isp-m0 kept reporting "Running Mode: Day," and the
always-on substream showed a visibly magenta, IR-lit image all evening.
The fix, in src/hal/hal_ingenic.c (fs_kick_chn0(), state machine in
src/hal/fs_ref.h): after every running_mode apply, and once per request
that changed hflip/vflip, the HAL makes sure FrameSource channel 0 runs:
- chn0 idle: enable it (a real
EnableChnedge, which also re-applieshflip/vflip/running_mode), let it run 500 ms (roughly 12 frames at 25 fps, generous margin over the 1–2 frames empirically needed to latch the mode), then return. The kick keeps its reference for anotherMS_IDLE_STOP_US(2 s) and the idle producer loops release it, so chn0 is disabled about 2–3 s after the kick instead of right after the 500 ms. - chn0 already running (a client, the motion pin or a previous kick): no edge and no wait.
- chn0 kept alive (
general.fs_keepalive, default1on every SoC since v1.12.0; up to v1.11.1 the default auto was on only on open-stack T23/T40/T41): the kick does nothing at all; chn0 has been running since start, so the Set latches on the next frames (assumed, not yet measured on that stack). The kick only takes (or extends) the same 2 s hold, so the channel cannot idle-stop before the Set has latched.
Up to v1.9.33 the kick dropped its reference right
after the 500 ms, even when chn0 was already running (and then still waited
500 ms). That made the end of every kick a DisableChn, and at start-up the
next thing that happened was the first timelapse shot, an EnableChn a few
milliseconds later. A disable followed by an enable within a few hundred
milliseconds while another channel streams is the pattern that hangs
open-stack T23/T41 SoCs (dev_notes/FS_KEEPALIVE_2026-10.md).
On the vendor SoCs the latch behaviour is unchanged: chn0 still runs at least
500 ms after an idle-time Set, now longer. What differs is that chn0 is
disabled 2–3 s later, and that a consumer arriving inside those seconds
finds it running instead of re-enabling it.
This means the return code of SetISPRunningMode() is now also checked
and logged (LOGW on failure) rather than ignored outright, and a
diagnostic (debug-level only) read-back via GetISPRunningMode is
logged, with the caveat that this readback is a libimp userspace value
(there is no kernel read path on T23) and so isn't by itself proof the
pipeline actually latched — it's fs_kick_chn0() that provides
the actual fix.
Machine-readable log lines¶
Two lines are deliberately fixed-format, columnar key=value text: fleet
dashboards grep for them, and a reworded sentence would empty their count
silently, the same failure the 2026-08-17 hand-run exposure campaign had
when its script quietly stopped being called. Both use -1 for "not
measured", the same convention as the GET /control status fields, and both
avoid spaces inside a value.
Every switch, at LOGI on the same line as the human-readable reason:
switching to night (heartbeat): daynight night [mode=night exp=6182 ref=-1 bar=768]
The [...] tail: mode is the mode being switched to (day/night);
exp is the exposure index at the moment of the switch; ref is the night
reference at that moment (-1 when not currently in night, e.g. a
day→night switch); bar is the day threshold (day_gain) — the same value
on every switch line regardless of direction, since it is read once per tick
rather than chosen per branch.
Every silent probe, once per verdict, at LOGD, at the point where all
branches have decided:
probe: r=1.05 lit=3350 dark=3520 hr=199 verdict=day mode=night ref=12098 why=brightening
r is the IR ratio (dark / lit, -1 if the probe got no usable reading);
lit/dark are the two raw readings compared — with the illuminator on and
off — the same pair the hand-run campaign used to collect by script (-1
alongside r=-1); hr is the AE headroom in log2 units at the dark
reading; verdict is day, night, or escalate (inconclusive — the
audible probe was asked for instead); mode is the automaton's mode at the
time of the probe; ref is the night reference (-1 outside night); why
is the trigger that asked for this probe (jump, trend, heartbeat,
boot measure, requested, …) with spaces replaced by underscores, since a
key=value value can't carry one; lit_hr (appended 2026-08-21) is the AE
headroom at the lit reading, the number that decides whether r compared
two measurements or two clips.
Since 2026-08-20 this line is LOGD — a measurement, not an event (see
Logging). A collector that counts or plots it needs
general.debug_modules = daynight
in /etc/timps.conf; without it the line simply stops, and a counting
dashboard reads "no probes" where it should read "not logged". The
switching to line above stays at LOGI unconditionally.
Idle-Dark (IR LED off while nobody watches)¶
Since v1.10.0, off by default: daynight.ir_idle_off = 1,
daynight.ir_idle_linger_s (default 60 s, 0..3600), both live via
/control. WebUI: Day / Night → Settings → "IR light when idle".
What it does¶
At night the IR LED normally burns all night, whether anyone looks or not.
With Idle-Dark, once no picture consumer has been there for
ir_idle_linger_s, timps switches only the IR LED off. The IR-cut filter
stays open and the ISP stays in night mode, so nothing clicks and no colour
switch happens. Picture consumers are video and JPEG subscribers (RTSP,
WebRTC, fMP4, MJPEG, SRT, /snapshot.jpg, timelapse, the recorder), motion
detection's FrameSource and a set jpeg.snapshot_path. Audio-only clients do
not count. A consumer inside the linger is not an event; the linger covers
10 s snapshot polling and DESCRIBE→PLAY. Keep it longer than
timelapse.interval_s: with both at 60 s the linger can run out on the tick
before each shot, and every shot lights the LED once (set e.g. 90 s).
Requirements¶
- FrameSource keepalive (
general.fs_keepalive; on by default since v1.12.0,auto= on only for OpenIMP builds on T23/T40/T41). Without it the AE stops when the last consumer leaves and nothing can be measured while idle. With0, or with auto (also a misspelt value) on other SoCs, it is off; Idle-Dark is untested outside OpenIMP T23/T40/T41. - A switchable illuminator:
daynight.irprobe_cmd(defaulttimps-irprobe) with the verbsidle(LED off) andstate(printson/off). Firmware must ship the new hook (thinginociaofrom commit67fbc42e9, 2026-10; the failsafe below from3af7d99e4). The illuminators it switches are the ones ticked indaynight.controls(IR 850/940 nm); a ticked white light makes it refuse, because it would light every reading. - Automatic switching by light level:
daynight.enabled = 1anddaynight.mode = auto. Calendar mode (schedule) hands the LED back.
Why off by default¶
With 850 nm LEDs the red glow is visible. When it goes out, anyone in front
of the camera can tell that nobody is watching or recording - "IR off" means
"nobody is watching". The first seconds of a new stream are also darker, and
the decision path has not yet been measured on enough cameras (see the
measurement list in dev_notes/DN_IDLE_DARK_2026-10.md). For 940 nm or
indoor cameras the tell does not matter.
How it decides, in plain words¶
- Going dark. At night, in auto mode, after the boot measurement, with
no probe in flight, at least 5 s after the last switch and with the
linger over, timps runs
<irprobe_cmd> idle. The next 8 s are a silent probe that does not relight: a large drop (ratio ≥ 2) confirms night, a small one with exposure reserve left means the room is lit (day, one switch, nobody watches). Anything unclear stays night and dark. An unwatched boot whose silent probe proves night goes dark straight away. - Measuring while dark. The IR-cut is still open and the LED off, so the AE now reads the ambient light without the LED ("noir"). timps keeps that reading; the LED is re-asserted off after 8 s, 16 s and then every 10 min in case the board script lit it. Each re-assert also renews the hook's 15 min failsafe (step 7).
- Freshness check. If the exposure has not moved at all in the 8 s after the LED went off, the AE is not running without a consumer on this SoC/stack. timps logs a WARN, turns the LED back on and does not try again until the next day/night switch.
- Dawn without a viewer (noir day rule). Noir below
day_gainforprobe_confirm_s, and the hook'sstatesays the LED really is off → switch to day. Noir is optimistic (the filter is open), so if the day optics turn out too dark, path A switches back within ~30 s and timps learns how much darker the scene gets with the filter closed (noir_cost); that veto stops the same false day the next morning. - A viewer arrives (wake). The subscribe kicks the day/night thread
(not the next 2 s tick): the LED goes on at once (one hook call). If the
noir reading is valid, already below
day_gainandstateconfirms the LED is off, timps switches to day instead and the LED stays off. - Warm-up hold. After a wake, JPEG grabs (
/snapshot.jpg, timelapse) wait for the AE for ~3.5 s, at most 5 s. Streams are not held. - Giving the LED back.
ir_idle_off = 0,daynight.enabled = 0,mode = schedule, a failing re-assert and a timps stop all turn the LED on. Iftimpsddies while the LED is off (SIGKILL, OOM, a crash), nothing in the firmware restarts it. The hook'sidlearms a 15 min watchdog that each re-assert renews; once timps stops renewing it, it relights the LED, unless the board has switched to day meanwhile. The picture is therefore dark for at most ~15 min after the last re-assert, and the camera stays without a streamer untilS95timps start; that start's boot probe then decides afresh. A hook without the watchdog (firmware before3af7d99e4) leaves the LED off until that start.
What the user sees¶
- The IR glow goes out about
ir_idle_linger_safter the last viewer. - A new viewer: LED on within one hook call; the first 1-4 s of video are dark or overexposed while the AE settles on the LED.
/snapshot.jpgafter a dark period answers up to ~3.5 s (max 5 s) later than usual. Home Assistant's default timeout is 10 s.- WebUI chip: "IR off (idle)", "IR on · watched", "IR on · off in N s", "IR on · going dark", "ready (day)", "off" or "not available: …". The page's own preview counts as a viewer.
Trade-offs¶
- The 850 nm tell above.
- First-snapshot delay after every wake; a poll slower than the linger pays one LED on/off pair and one held snapshot per poll.
- Heartbeat probes in the dark are audible (every
heartbeat_s/heartbeat_max_s, 4-18 h by default): a silent probe cannot work while the LED is already off, so the heartbeat uses the IR-cut probe. - Cameras with a permanent consumer (Frigate/NVR,
record.enabled,motion.enabled,jpeg.snapshot_path) never go idle: no effect, no cost.
Troubleshooting: daynight.ir_idle.reason¶
GET /control → daynight.ir_idle = {cfg, available, reason, dark,
watched, linger_left_s}; caps.daynight.ir_idle says whether this start can
use it at all (keepalive).
reason |
Meaning | Fix |
|---|---|---|
disabled |
ir_idle_off = 0 (cfg 0), or daynight.enabled = 0 |
turn the switch / automatic switching on |
schedule |
daynight.mode = schedule |
"Decided by: Light level" (mode = auto) |
keepalive |
no FrameSource keepalive on this start (one WARN) | general.fs_keepalive = 1 (the default since v1.12.0; check for 0 or a typo, which parses as auto: on only for OpenIMP T23/T40/T41) + restart |
no_illuminator |
daynight.irprobe_cmd empty, or the illuminator was retired after two failed switches |
set the hook, tick IR 850/940 nm, check the board's IR GPIOs; a restart re-tests |
white_light |
the hook refused idle (exit 3): white light is in daynight.controls |
untick "White light" |
hook |
idle exit 1/2 (hook without the verb, nothing switchable) or repeated failures |
update the firmware's timps-irprobe; switching the key off and on retries |
available = 1 but the LED never goes out: someone is watching
(watched = 1, check GET /control?clients=1, motion detection, the
recorder), or the log has exposure did not move in 8s with the illuminator
off (the AE stops without a consumer here: leave the feature off on this
camera). The WARN lines are listed in docs/ai/troubleshooting.md §3.9.
How to turn it off¶
POST {"daynight":{"ir_idle_off":0}} (or the WebUI switch): the LED comes
back on at once and the key is persisted.
Live control and status¶
| Action | Effect |
|---|---|
POST /control {"daynight":{"enabled":false}} |
Switches to manual mode; the thread keeps sampling for status but stops forcing switches. |
POST /control {"daynight":{"mode":"schedule",...}} |
Switches decision source; validated against auto/schedule (and the legacy sensor/time/sun spellings) before being applied. |
POST /control {"image":{"running_mode":1}} |
Manually forces the ISP mode (what the board switch script itself calls). Re-posting the same value still re-drives the ISP (see HTTP /control API Reference) — necessary precisely because of the latch quirk above. |
POST /control {"daynight":{"probe":1}} |
Arms one look (IR illuminator off 8 s) for the next tick, without waiting for a jump, a colour change or the 45-min timer. A command, not a setting — counted in accepted/rejected like record.clip. Rejected when there is no daynight.irprobe_cmd configured or looks were turned off for the session after two hook failures. |
GET /control "daynight" object |
Read-only status: enabled, mode (0 day/1 night), brightness (%), total_gain (IMP [24.8] linear), exposure (the index the decision actually runs on — plot this one when diagnosing), ae_luma, night_baseline (the illuminator-lit level at the last look) and day_trigger (the bar a look must read below to ask for a check; both always published since v1.10.1), since v1.11.0 color_trigger (on, off = color_pct 0 / no irprobe_cmd / schedule / disabled, unavailable = no AWB statistics in this libimp) and color_dev_pct (colour change of the lit picture since the last look in %, -1 without a reference), the configured keys, the Idle-Dark state ir_idle (cfg, available, reason, dark, watched, linger_left_s, see above), and — always — today's computed sun_computed_sunrise/sun_computed_sunset. |
GET /events?stream=daynight |
Pushes the same status object on connect and then whenever mode flips, brightness moves ≥1%, or gain moves ≥5% relative (or ≥8 absolute near zero) — see HTTP /control API Reference. |
GET /control?dn_history=1 |
The decision series (gain, exposure index, ae_luma, brightness, mode, one sample per 10 s) out of the in-RAM ring daynight.history_s sizes — off by default. Unlike the SSE stream this keeps accumulating with no client connected, which is what lets the WebUI tuning graph plot hours nobody was watching. Cursor-paged; see HTTP /control API Reference. |
See Configuration Reference
for the full key table (including which fields are settable via
/control vs. config-file-only).