"""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, )