feat(hour/day derivation): hour classification, monitoring periods, day aggregates, pruning
Hour classification (PR-4): - Powered-off: POH delta < 90% of wall-clock span - Active: DUW delta >= 256 MiB - Idle: powered on + sampled + below active threshold - Unknown: unsampled without POH evidence - Four splits sum to exactly wall_clock_seconds - Disabled time is never an hour state Monitoring periods (FL-8): - ensure_period_open: opens period at run moment if none exists - close_period: closes with end cause - is_inside_period: checks timestamp against period bounds - Never backdated; wall-clock outside periods excluded from denominator Day aggregates (ST-4, PR-5): - Derived monotonically from hour rows - UTC-bounded; no 23/25-hour days - Coverage: known seconds / period wall-clock - Gap hours inside periods contribute unknown seconds - Hours outside periods excluded entirely - No absent hour interpolated/estimated/fabricated (FL-3) Raw sample pruning (ST-5): - Prunes samples older than 14 days - Hour observations and day aggregates retained indefinitely Closes #22
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
"""Hour classification per spec §5.1.
|
||||
|
||||
Each UTC hour is classified by named constants, in this order of evidence:
|
||||
- Powered-off: power-on-hours delta < 90% of wall-clock span
|
||||
- Active: DUW delta >= 256 MiB in the hour
|
||||
- Idle: powered on, sampled, below active threshold
|
||||
- Unknown: everything else (unsampled without POH evidence)
|
||||
|
||||
Four splits sum to exactly wall_clock_seconds. Disabled time is never an
|
||||
hour state — it is wall-clock outside monitoring periods (§5.2).
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
|
||||
# Spec §5.1: Active hour threshold — 256 MiB DUW delta
|
||||
ACTIVE_THRESHOLD_BYTES = 256 * 1024 * 1024 # 256 MiB
|
||||
|
||||
# Spec §5.1: Powered-off threshold — 90% of wall-clock span
|
||||
POWERED_OFF_THRESHOLD_PERCENT = 0.90
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HourSplit:
|
||||
"""Usage-habit split for one UTC hour. Fields sum to wall_clock_seconds."""
|
||||
seconds_active: int
|
||||
seconds_idle: int
|
||||
seconds_powered_off: int
|
||||
seconds_unknown: int
|
||||
|
||||
|
||||
def classify_hour(
|
||||
wall_clock_seconds: int,
|
||||
poh_delta: int,
|
||||
duw_delta: int,
|
||||
dur_delta: int,
|
||||
sampled_seconds: int | None = None,
|
||||
) -> HourSplit:
|
||||
"""Classify a UTC hour into the four usage-habit states.
|
||||
|
||||
Args:
|
||||
wall_clock_seconds: Total seconds in this hour boundary (3600 for a
|
||||
full hour, less for partial-hours at period edges).
|
||||
poh_delta: Power-on-hours delta since previous sample (in seconds).
|
||||
duw_delta: Data-units-written delta since previous sample (in bytes).
|
||||
dur_delta: Data-units-read delta since previous sample (in bytes).
|
||||
sampled_seconds: Seconds within this hour covered by a sample.
|
||||
None or 0 means no sample fell in this hour.
|
||||
|
||||
Returns:
|
||||
HourSplit whose four fields sum to wall_clock_seconds.
|
||||
"""
|
||||
if sampled_seconds is None:
|
||||
sampled_seconds = 0
|
||||
|
||||
# Clamp sampled_seconds to wall_clock_seconds
|
||||
sampled_seconds = min(sampled_seconds, wall_clock_seconds)
|
||||
|
||||
# --- Decision order per spec §5.1 ---
|
||||
|
||||
# 1. Powered-off: POH delta < 90% of wall-clock span
|
||||
powered_off_threshold = wall_clock_seconds * POWERED_OFF_THRESHOLD_PERCENT
|
||||
if poh_delta < powered_off_threshold:
|
||||
return HourSplit(
|
||||
seconds_active=0,
|
||||
seconds_idle=0,
|
||||
seconds_powered_off=wall_clock_seconds,
|
||||
seconds_unknown=0,
|
||||
)
|
||||
|
||||
# 2. Active: DUW delta >= 256 MiB
|
||||
if duw_delta >= ACTIVE_THRESHOLD_BYTES:
|
||||
return HourSplit(
|
||||
seconds_active=wall_clock_seconds,
|
||||
seconds_idle=0,
|
||||
seconds_powered_off=0,
|
||||
seconds_unknown=0,
|
||||
)
|
||||
|
||||
# 3. Idle: powered on, sampled, below active threshold
|
||||
# Unsampled portion within the hour is unknown
|
||||
if sampled_seconds > 0:
|
||||
return HourSplit(
|
||||
seconds_active=0,
|
||||
seconds_idle=sampled_seconds,
|
||||
seconds_powered_off=0,
|
||||
seconds_unknown=wall_clock_seconds - sampled_seconds,
|
||||
)
|
||||
|
||||
# 4. Unknown: unsampled without POH evidence
|
||||
return HourSplit(
|
||||
seconds_active=0,
|
||||
seconds_idle=0,
|
||||
seconds_powered_off=0,
|
||||
seconds_unknown=wall_clock_seconds,
|
||||
)
|
||||
Reference in New Issue
Block a user