Sensor Staleness — Current State and Improvement Proposals¶
Status: assessment + proposals, partly implemented
This page describes the current state of staleness detection and five proposals. All measurements are from 13–16 Aug 2026 on the production system. Related: Tageswechsel & Energiezählung (German) — see relation at the end of this page.
| Proposal | State |
|---|---|
| 1 — date check for daily forecasts | implemented in v2.0.4 |
2 — unavailable for hardware |
already in place, nothing to do |
| 3 — price margin 6 h + 2 min | implemented in v2.0.4 |
| 4 — Solcast data freshness | implemented in v2.0.4 |
| 5 — plausibility check on the curve shape | deliberately dropped (see proposal 5) |
Trigger
Observed false alarm on the live system: the dashboard reports
solcast_pv_forecast_prognose_heute as "stale" regardless of how old the value
really is. Measured on the production system (18:14 UTC): last_updated = 08:31:25
→ age 9 h 43 min, while the configured threshold (forecast_max_age_sec, 8 h)
already classifies that as outdated. The cause is now proven in the Solcast
integration's source code (see below) — this is not a configuration problem but a
flaw in the checking method itself.
This is the second occurrence of this bug class in the project. The first
(battery_soc_entity, fixed in v2.0.2) is the precedent for the proposals below.
Mechanism (in brief)¶
HAWebSocketClient._parse_ts(state)
→ prefers state["last_updated"], falls back to state["last_changed"]
→ stored in _state_ts[entity_id]
get_state_age_sec(entity_id)
→ now(UTC) − _state_ts[entity_id] (None if never received)
is_stale(entity_id, max_age_sec)
→ age is None OR age > max_age_sec
Three constants cover every staleness check in the project (const.py):
| Constant | Value (current) | Intended for |
|---|---|---|
SENSOR_MAX_AGE_SEC |
300 s (5 min) | Power sensors, SoC liveness proxy |
FORECAST_MAX_AGE_SEC |
28,800 s (8 h, since v2.0.1) | Solcast sensors |
PRICE_MAX_AGE_SEC |
21,600 s (6 h, since v2.0.1) | Electricity price |
Dead feature: get_state_value(entity_id, max_age_sec=None) has an optional
staleness parameter that is never passed a value at any call site in the codebase
(verified: grep -n "get_state_value(" *.py). Every actual staleness check goes through
the separate, explicit is_stale() calls listed below.
Two obvious shortcuts — and why only one holds up¶
Before the numbers: two ideas suggest themselves when you see this problem. Both were checked against the actual integration source on the production system. One does not hold up at all; the other only halfway.
Idea A: "Just use last_reported instead of last_updated"¶
Home Assistant maintains three timestamps per entity:
| Field | Advances when … |
|---|---|
last_changed |
the value changes |
last_updated |
value or attributes are written ← what miniEMS uses |
last_reported |
the entity writes at all, even with an identical value |
last_reported is in fact included in the REST JSON (verified live), so switching would
be trivial. It still gains nothing here. All three fields only advance when the
integration actually calls async_write_ha_state(). They measure "when did the
integration last write" — not "when did it last reach the source".
And for the daily forecasts, the Solcast integration deliberately does not write
(custom_components/solcast_solar/sensor.py, _handle_coordinator_update):
if self._update_policy == SensorUpdatePolicy.DEFAULT and not (
self._coordinator.date_changed or self._coordinator.data_updated):
return # ← no async_write_ha_state()
get_sensor_update_policy() (same file) grants EVERY_TIME_INTERVAL to only nine keys —
among them ENTITY_FORECAST_REMAINING_TODAY and ENTITY_POWER_NOW. Everything else,
including the "today" and "tomorrow" daily totals, gets DEFAULT.
Measurement on the production system (18:14 UTC), all sensors from the same integration and the same coordinator:
| Sensor | last_changed = last_updated = last_reported |
Age | Policy |
|---|---|---|---|
…_aktuelle_leistung (power now) |
18:10:00 | 4 min | EVERY_TIME_INTERVAL |
…_prognose_verbleibende_leistung_heute (remaining today) |
18:10:00 | 4 min | EVERY_TIME_INTERVAL |
…_prognose_heute (forecast today) |
08:31:25 | 9 h 43 min | DEFAULT |
…_prognose_morgen (forecast tomorrow) |
08:31:25 | 9 h 43 min | DEFAULT |
The integration is demonstrably alive — it writes every five minutes. Only the daily
total is not rewritten, because nothing changed semantically. For prognose_heute all
three timestamps are identical: last_reported would reproduce the false alarm
exactly. The same holds for the price sensor
(sensor.deye8k_current_electricity_price: last_changed = last_reported = 16:00:00).
Idea B: "Doesn't the sensor just go unavailable when the source is gone?"¶
available is a purely opt-in property of each integration. The two integrations
miniEMS depends on decide it in opposite ways:
Solarman/Deye (custom_components/solarman/entity.py:38) — the inverter:
def available(self) -> bool:
return self.coordinator.last_update_success and self.coordinator.device.state.value > -1
→ Yes. Modbus connection lost ⇒ unavailable. A reliable, immediate signal.
Solcast (custom_components/solcast_solar/sensor.py:690):
_sensor_data comes from the forecast cache persisted to disk, not from an API call.
API down, quota exhausted, internet gone — the cache stays populated, the sensor stays
available and serves yesterday's forecast indefinitely. → No, it never goes
unavailable.
The unavailable half is already implemented in miniEMS: get_state_value() returns
None for unavailable/unknown/"" (ha_ws_client.py:67), and
_build_sensor_warnings turns that into "Sensor unavailable". For the Deye sensors this
is therefore the dependable signal — the age check there is essentially redundancy.
What follows from both ideas¶
These are two orthogonal failure modes that today's age check conflates:
- Connection dead →
unavailable. Already solved, reliable for all hardware sensors. - Connection alive, data semantically outdated (Solcast forecast from yesterday) →
neither
unavailablenor any timestamp catches this. Only a semantic check helps.
Current state — every call site in the project¶
| # | Location | Entity checked | Constant | Real update cadence (live + proven in integration source) | Verdict | Solution available |
|---|---|---|---|---|---|---|
| 1 | ems_controller.py:358 (_decide, SoC liveness proxy) |
battery_power_entity |
sensor_max_age_sec (300 s) |
continuous, ~15 s | ✅ appropriate (v2.0.2 fix — reference case) | ✅ |
| 2 | ems_controller.py:376-377 (_decide, PV surplus precondition) |
pv_power_entity, load_power_entity |
sensor_max_age_sec (300 s) |
continuous, ~15 s | ✅ appropriate | ✅ |
| 3 | ems_controller.py:446 + warning banner |
electricity_price_entity |
price_max_age_sec (21,720 s / 6 h + 2 min, since v2.0.4) |
time-driven schedule, changes only at tariff-tier boundaries; longest window exactly 6 h (06:00–12:00, activation_rules) |
✅ fixed — previously an exact tie (threshold == longest window) | ✅ implemented |
| 4 | ems_controller.py:482 (_forecast_remaining_kwh) |
solcast_remaining_today_entity |
forecast_max_age_sec (28,800 s / 8 h) |
policy EVERY_TIME_INTERVAL (5-min write cadence), but the value only moves while the curve runs: legitimately flat for 5 h 50 min overnight (measured on two consecutive nights) |
❌ blind to the real failure mode: the value is carried forward from cache + clock and stays entirely plausible even with a days-dead API | ⚠️ proposal 5 detects integration failure in minutes rather than hours — data freshness stays open |
| 5 | ems_controller.py:460 (_should_grid_charge, tomorrow fallback) |
solcast_tomorrow_entity |
forecast_max_age_sec (28,800 s / 8 h) |
only on forecast fetch or date rollover — policy DEFAULT |
✅ fixed — date check instead of forecast_max_age_sec (v2.0.4); the v2.0.1 grid-charge fix now applies at night again |
⚠️ integration failure solved, data freshness still open (proposal 4) |
| 6 | ems_controller.py:564 (warning banner) |
solcast_today_entity |
forecast_max_age_sec (28,800 s / 8 h) |
only on forecast fetch or date rollover — policy DEFAULT; confirmed live as the cause of the reported false alarm (9 h 43 min) |
✅ fixed — date check instead of forecast_max_age_sec (v2.0.4) |
⚠️ integration failure solved, data freshness still open (proposal 4) |
| 7 | Generic required list (warning banner) |
pv_power, battery_power, grid_power, load_power |
sensor_max_age_sec (300 s) |
continuous | ✅ appropriate | ✅ |
Legend for the last column: ✅ = both failure modes (connection dead / data outdated) covered · ⚠️ = only one of the two · ❌ = no effective detection at all.
battery_soc_entity itself has had no age check since v2.0.2 — only a presence check
(get_state_value(...) is None). That is the already-solved instance of this bug class.
Worth noting: #4 and #6 come from the same integration and share the same constant even though their cadences have nothing in common — it fits #4 (5 h 50 min measured flat period against an 8 h threshold) while being too tight for #6. That one and the same value fits one sensor and produces false alarms on another from the same integration is demonstration enough that "age" cannot be the load-bearing measure here.
Two corrections to earlier versions of this page
(1) #4 was initially classified as "✅ appropriate, large margin". That obscures
the fact that the check cannot see the actual failure mode. #4 is the only Solcast
value that feeds _should_grid_charge directly.
(2) This page then claimed the check "can structurally never fire, because the
sensor is rewritten every 5 minutes". That was also wrong. The five-minute cadence is
the write cadence; last_updated only advances on an actual value change.
Measured across two nights, the value legitimately sits still for 5 h 50 min
(22:00→03:50 UTC, identical on both nights) — so the 8 h constant has roughly 2 h of
margin and is perfectly effective against integration failure, merely very slow. It
is blind only to outdated data.
How much does #5 actually matter? — reachability of the tomorrow branch
The affected branch in _should_grid_charge is only reached when both hold: cheap
tariff and (remaining missing or remaining ≤ 1.0 kWh). That combination is
rarer than it first appears:
- Summer, healthy forecast sensor:
remaining ≤ 1.0 kWhonly occurs from about 20:15 local onwards (measured), while the cheap windows are 02:00–06:00 and 12:00–16:00. The conditions never overlap — the branch is unreachable and the bug is inconsequential. - Winter: between 02:00 and 06:00,
remainingholds the daily forecast of the new day (reset at local midnight). On an overcast December day that can fall below 1.0 kWh ⇒ branch reached, and 02:00–06:00 lies inside the dark window (21:00–06:00) ⇒ it charges.prognose_morgenis then guaranteed stale (last fetch the previous day, ~17.5 h), so the "tomorrow will refill it" guard is skipped. This is where the bug costs money — precisely when grid charging matters at all. - Forecast sensor outage:
remaining is Nonesatisfies the condition too, at any time of day. If Solcast fails during a cheap window, the branch becomes reachable in summer as well.
In the 12:00–16:00 cheap window the dark-window test correctly prevents charging, regardless of forecast state.
Related finding outside the staleness logic — fixed in v2.0.4
_should_grid_charge has no hysteresis — the return in ems_controller.py:475 is
a bare threshold comparison (bat_kwh_free > remaining × 1.2 + 1.0). Its only damping
is the mode_dwell_sec dwell time (300 s) in _commit(). The PV path, by contrast,
has an asymmetric hysteresis via pv_charge_hysteresis_frac that fully absorbs the
measured forecast steps (+0.35 / +0.13 kWh, see proposal 5). Since remaining moves by
up to 0.47 kWh per 5-minute interval during the day, a battery sitting near the
threshold can oscillate in the grid-charge path. Not a staleness problem, but the same
root cause — jumpy forecast data — and noted here for that reason.
Root cause¶
Three constants effectively cover five distinct update-cadence classes:
| Class | Example | Real cadence | Matching constant? |
|---|---|---|---|
| a) continuous | power sensors | seconds | SENSOR_MAX_AGE_SEC — yes |
| b) fixed write cadence, value-driven | Solcast "remaining", "power now" | 5 min during the day, up to 5 h 50 min without a value change overnight | FORECAST_MAX_AGE_SEC — yes, ~2 h margin |
| c) several times a day, fixed points | electricity price (ToU tariff) | up to exactly 6 h, schedule fixed | PRICE_MAX_AGE_SEC — right magnitude, just no margin |
| d) event-driven, ~1×/day | Solcast "today"/"tomorrow" (totals) | only on fetch/date rollover | no constant of its own — shares (b), both wrong |
| e) practically never | SoC, battery capacity | hours to days, legitimately | no age check needed (already fixed) |
The deeper cause, however, is not the constants. For class (d) no age limit can be structurally correct: the same forecast value is fresh at 09:00 and still correct at 23:00 — the timestamp's age says nothing about validity in either case. Raising the threshold merely postpones the problem.
Improvement proposals¶
1. Daily forecasts: check the date, not the age ✅¶
Implemented in v2.0.4
HAWebSocketClient.is_stale_daily(entity_id, grace_sec) asks "was it written
today?" instead of "how old is it?". Used at both sites: the tomorrow fallback
in _should_grid_charge (#5) and the warning banner (#6, which now also covers
solcast_tomorrow_entity). The grace period DAILY_VALUE_GRACE_SEC = 900
covers the minutes after midnight in which Solcast has not rewritten yet.
The comparison runs in local time on purpose: the source rolls the day over
in the installation's timezone while _parse_ts stores UTC. Under CEST a UTC
comparison would be wrong every night between 22:00 and 00:00 UTC.
Instead of asking "how old is the timestamp?", ask the semantically correct question: "Is the timestamp from today?"
For class (d) this is answerable exactly, because the Solcast integration is guaranteed
to write on every date rollover (coordinator.py):
self.tasks[TASK_LISTENERS] = async_track_utc_time_change(
self.hass, self._update_integration_listeners, minute=range(0, 60, 5), second=0
)
_update_integration_listeners therefore runs every five minutes and sets:
current_day = dt.now(self.solcast.options.tz).day
self._date_changed = current_day != self._last_day
When date_changed is true, the early-return condition in _handle_coordinator_update
no longer applies and all DEFAULT-policy sensors are rewritten. This yields a hard
guarantee:
As long as the Solcast integration is running,
prognose_heuteandprognose_morgencarry a timestamp from the current day within five minutes of local midnight. A timestamp from yesterday necessarily means the integration has stopped.
This makes the check self-calibrating: no new constant, no new config field, no additional entity, and it stays correct across year boundaries and DST changes.
Two implementation notes:
- Use the local timezone. Solcast evaluates the date rollover in local time
(
dt.now(self.solcast.options.tz)), whereas_parse_ts()returns UTC. Under CEST (UTC+2), local midnight is 22:00 UTC the previous day — a date comparison in UTC would be wrong for two hours. The comparison must happen in the HA installation's local timezone. - Small grace period. Between 00:00 and 00:05 local time the timestamp is still legitimately from yesterday. A tolerance of ~15 minutes after midnight avoids a daily false alarm inside that five-minute window.
Affects #5 and #6. Resolves the reported false alarm and closes the gap in the v2.0.1 grid-charge fallback.
Scope of this proposal
The date check answers "is the integration running today?" — not "how old is the forecast data?". It does not cover the second case; see proposal 4.
2. Hardware sensors: unavailable is already the right signal¶
For #1, #2 and #7 the Solarman integration reports unavailable on connection loss —
faster and more reliable than any age check, and already evaluated in
get_state_value(). The 300-second check there is not wrong, but largely redundant; it
can remain as a second line of defence.
3. Electricity price: no semantic equivalent — a small margin suffices ✅¶
Implemented in v2.0.4
PRICE_MAX_AGE_SEC = 21720 in const.py, plus a _v13_to_v14() migration that
raises existing configurations sitting on exactly the old default of 21600. A
differing value — i.e. one deliberately set — is left alone. Verified live on the
devcontainer:
Migration v13→v14: price_max_age_sec was exactly the longest tariff window
(21600s/6h) – raised to 21720s for clearance.
For #3 there is neither a date reference nor a liveness proxy: the price sensor is
written only on tier changes (last_changed = last_reported, confirmed live).
The upper bound is, however, known exactly: the tariff is purely time-driven, the schedule is fixed, and the longest window is exactly 6 h (06:00–12:00). No market dynamics can change that. So no generous buffer is needed — only enough clearance to avoid the "threshold == longest window" exact tie:
A higher value would only degrade detection time without covering any real case.
The cleaner solution would be a plausibility check against the entity's
activation_rules schedule — that schedule is present and describes the tariff profile
completely. It requires access to entity attributes, which miniEMS currently has nowhere;
this is a touchpoint with the
grid-friendly charging roadmap, which proposes
exactly that access as its first step.
4. Solcast data freshness (#4, #5, #6) ✅¶
Implemented in v2.0.4
get_state_datetime() reads entities whose state is a timestamp —
get_state_value() ends in float(raw) and cannot. What is evaluated is the
value of solcast_last_fetch_entity, not its timestamp.
SOLCAST_DATA_MAX_AGE_SEC = 30 h, derived from measurement rather than guessed:
6/6/6/5/4 successful fetches per day on five consecutive days on the
production system, longest legitimate overnight gap 15.5 h (12:44 UTC →
04:11 UTC). Shorter winter days push that towards an estimated 19 h; 30 h leaves
margin while still catching a stalled API within about a day.
Applied in two places: _forecast_remaining_kwh() returns None so no control
decision rests on days-old data, and the warning banner reports it. With no
entity configured the check simply does not apply, rather than raising a false
alarm.
For the three Solcast checks, only the "integration dead" failure mode is addressed so far. There is a second one that none of the signals above detects:
The Solcast API is unreachable (quota exhausted, internet gone, service disrupted) while the disk cache keeps serving unchanged.
In that state everything looks healthy:
| Signal | Behaviour in this case | Detects the fault? |
|---|---|---|
unavailable |
cache populated → sensor stays available |
❌ |
last_updated / last_reported |
advance | ❌ |
| Date check (proposal 1) | date rollover writes unconditionally → timestamp is from today | ❌ |
| Age check on #4 | curve carries on from cache + clock → age stays in the normal range | ❌ |
| Plausibility check (proposal 5) | curve shape remains entirely plausible | ❌ |
The most serious case is #4, because that value feeds _should_grid_charge directly:
the grid-charge decision can run on a days-old forecast without any warning appearing.
Only once the cache no longer holds data for the current day — after roughly seven days —
does _sensor_data become None and the sensor flip to unavailable, at which point
the existing presence check applies. The days before that are blind.
Candidate for the gap: the value of
sensor.solcast_pv_forecast_zeitpunkt_letzter_api_abruf — explicitly its content, not its
timestamp (see the box below). Proven in the source (solcastapi.py:561):
@property
def last_updated(self) -> dt | None:
"""When the data was last updated.
Returns:
dt | None: The last successful forecast fetch.
"""
return self.data[LAST_UPDATED].astimezone(self.tz) if self.data.get(LAST_UPDATED) is not None else None
The value comes from the persisted dataset and is therefore exactly the age of the
forecast data, independent of how often HA rewrites the sensors. Verified live:
2026-08-15T08:31:25 at a measurement time of 18:14 UTC ⇒ forecast data 9 h 43 min
old.
Implementation obstacle: this is a datetime sensor. get_state_value() does float(raw)
and returns None for an ISO timestamp (ha_ws_client.py:69-72) — it would need a
datetime path in the client plus a config field for the entity, analogous to
load_consumption_entity in v2.0.3. A sensible threshold should be derived from the
actual fetch rhythm rather than guessed; that measurement, across several days, is still
missing.
5. Plausibility check on the curve shape (#4) — dropped¶
Deliberately not implemented
After proposal 4 this check no longer closes a gap. Its benefit would be pure
detection speed for a frozen sensor — minutes instead of hours — on a rare
failure mode. Data freshness is covered by proposal 4, connection loss by
unavailable.
Against that stand three refinements that would all have to be implemented and maintained correctly (midnight reset rather than morning, the night plateau at the daily maximum, tolerance for intraday forecast revisions). Each is a false- alarm source of its own. The ratio does not work out.
The analysis below stays as evidence — it is the measurement basis for understanding the sensor's behaviour, implementation or not.
Instead of asking when the value last arrived, check whether it behaves the way it
must. remaining_today has a mandatory daily shape: reset once, then decay to zero.
Measured across two full days (HA recorder, 356 data points, 13–15 Aug 2026):
| Phase | Observation (UTC / local = UTC+2) | Duration |
|---|---|---|
| Reset | 22:00 / 00:00 local: 0 → 44.37 and 0 → 33.68 respectively | once daily |
| Night plateau | 22:00 → 03:50, value constant at the daily maximum | 5 h 50 min (identical both nights) |
| Daytime decay | 03:50 → 19:00, monotonically falling, smooth | ~15 h |
| Evening zero | 19:00 / 21:00 local: exactly 0, constant thereafter | 3 h until reset |
| Upward steps | 14 Aug 04:19 (+0.3466) and 08:30 (+0.1271) — both at forecast fetch moments, off the 5-minute grid | 2× on one day, 0× on the other |
That yields three refinements to the obvious formulation "zero at night, otherwise falling, one rise in the morning":
- The rise sits on local midnight, not in the morning. It is the date rollover, not sunrise.
- Only the evening dark window is zero. In the pre-dawn hours the value sits at its daily maximum, constant for 5 h 50 min — a rule "dark ⇒ 0" would raise a false alarm every night between 00:00 and 05:50 local.
- Monotonicity only holds between forecast fetches. Solcast revises intraday; the measured steps were small at +0.35 and +0.13 kWh, but they are systematic and should be expected to be considerably larger on a changeable day. A strict monotonicity rule produces false alarms; it needs a tolerance, or an exemption at fetch moments (known from the value in proposal 4).
What the check delivers: it detects a frozen, implausible or miswired sensor during daylight within minutes instead of eight hours — over the course of the day the value moves every 5 minutes by amounts far above the noise floor (up to 0.47 kWh per interval measured). That is a genuine improvement in detection latency over the pure age check, and it covers faults none of the other measures can see.
What it does not deliver: it does not close the gap from proposal 4. The curve
shape is produced by the cached spline plus the clock — with a days-dead API, the reset,
night plateau, monotonic decay and evening zero all remain intact. Every single criterion
of this check would pass. For data freshness, the value of
zeitpunkt_letzter_api_abruf remains the only dependable signal.
Corrected proposal
An earlier version of this page recommended
sensor.solcast_pv_forecast_zeitpunkt_letzter_api_abruf as a liveness-proxy sensor
for Solcast — analogous to the battery_power_entity trick used for SoC. That was
wrong. Checked live, this entity reads 2026-08-15T08:31:25, and its own
last_changed is likewise 08:31:25 — exactly as old as prognose_heute itself.
Useless as a proxy. (Side finding: verwendete_api_abrufe last incremented at 10:36,
so the fetch timestamp even contradicts the integration's own counter.)
The entity is not worthless, though — only its role was misidentified: its timestamp is useless as a liveness proxy, whereas its value is precisely the data age that proposal 4 is missing. Confusing timestamp with content is the actual essence of this bug class — and it caught me out here too.
Not part of this page¶
This page describes the current state and proposals only — none of the points above are implemented. The actual implementation (date check for daily forecasts, price margin) is a separate step.
Explicitly still without a finished solution is proposal 4 (Solcast data freshness,
4/#5/#6): the signal source is identified and proven in the integration source, but the¶
client extension, the config field and an empirically grounded threshold are all missing.
Proposal 5 (plausibility check) and proposal 1 (date check) each improve detection of integration failure — proposal 5 markedly so in speed. Neither covers data freshness; that remains the one open point of this page, and it affects #4 of all things, the only Solcast value with direct influence on a control decision.
Relation to the day-rollover roadmap¶
While reviewing this page a second defect surfaced, covered in Tageswechsel & Energiezählung (German): miniEMS cuts the day 4 min 54 s ahead of the inverter. The two pages are more closely related than the topics suggest.
What the day-rollover rework does not fix here¶
The delimitation first, so no false expectation arises: switching to total counters with a self-computed delta fixes not one row of the table above. The Solcast sensors have no total counterpart, and the price and power sensors are untouched by energy accounting. The two efforts are independently implementable.
What it contributes nonetheless¶
1. Proposal 1 rests on a precondition proven there. The date check only works if
miniEMS's own day boundary reliably sits on local midnight — otherwise the Solcast date
would be compared against a shifted boundary. That is exactly what the day-rollover page
measures and confirms (TZ=Europe/Berlin in the container, cut observed live at
21:59:38 → 22:00:08 UTC). The precondition therefore need not be re-derived here — but if
anything changes about the day boundary there, proposal 1 must be re-checked.
2. The same figure of thought, twice. Both pages arrive independently at the same rule:
Do not trust the derived artifact — move one level closer to the raw source and compute the quantity you need yourself.
There: don't inherit the foreign day cut, read the monotonic lifetime counter and compute the daily delta yourself. Here: don't check the write timestamp of the derived sensor, read the source's own fetch time (proposal 4). Understand one and you understand the other.
3. The monotonicity safeguard generalises. The day-rollover page requires of the total
counter: a monotonically increasing value must never run backwards — if it does, that is a
reportable event, not a computation case. The same discipline applies to
zeitpunkt_letzter_api_abruf once proposal 4 is implemented: its value is monotonically
increasing too, and a backwards jump would be an integration defect, not a number to
interpret.
4. Coordination during implementation. Both efforts need new config fields and thus a
schema migration (migration.py, CONFIG_SCHEMA_VERSION). If they land in the same
release, they should share one schema version rather than two consecutive migration
steps. Both would additionally touch config_loader.py and templates/settings.html.