mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-06-11 01:14:45 +02:00
Replaces module-level magic numbers in 12 handlers with reads via the
v0.6-3a.1 typed accessor. Every default matches the prior hardcoded
value exactly, so first-deploy behavior is unchanged.
Handlers wired (43 keys across the 43-row registry):
wfigs cooldown_seconds, anchor_max_mi, broadcast_on_acres,
broadcast_on_contained
nws broadcast_severities, tombstone_msgtypes,
warning_suffix_promotes
usgs_quake regional_centroid, regional_radius_mi,
broadcast_pager_alerts, global_mag_floor,
regional_mag_floor, escalate_mag_floor
swpc geomag_kp_floor (extends G-scale down to Kp 5 when
lowered), flare_class_floor (R-scale
extended to M-class when lowered),
proton_pfu_floor
usgs_nwis parameter_codes, broadcast_on_recede
incident freshness_seconds, broadcast_on_update (Update path
re-implemented when toggled True:
magnitude step-up / delay doubling /
icon_category change)
tomtom_incidents drop_zero_magnitude, drop_non_present
state_511_atis skipped_states (case-insensitive match against both
state_code and primary_region suffix)
central severity_thresholds (immediate_min check ordered before
priority_max so the +inf clamp still
works)
dispatcher dedup_lru_max, cooldown_prune_size,
cooldown_prune_multiplier, dedup_db_retention_days
band_conditions swpc_freshness_seconds, hamqsl_url, hamqsl_timeout_s
geocoder (photon_url/timeout/radius/limit/town_osm_values/
h3_cache_max -- module-level constants kept as
backward-compat aliases; runtime reads via accessor)
pipeline Inhibitor.ttl_seconds + Grouper.window_seconds now
default to None, falling back to
adapter_config.pipeline.{inhibitor_ttl_seconds,
grouper_window_seconds}. Explicit constructor values
still win (test fixtures unchanged).
firms confidence_floor, frp_floor, bbox, dedup_distance_m
Schema:
v7.sql adds firms_pixels.dedup_key column + drops the old hardcoded
round(lat,5) UNIQUE INDEX, replaces with UNIQUE (dedup_key, acq_time,
satellite). The firms_handler quantizes lat/lon to
(dedup_distance_m / 111000) degrees at INSERT time -- meters-based
precision per Matt s spec, tunable via the GUI without schema changes.
SCHEMA_VERSION 6 -> 7. firms_pixels has 0 rows in production so no
backfill needed.
CODE preserved (Matt s rule): sentence templates, emoji literals, the
TomTom icon_map / ITD sub_type_map / Central adapter_map / category_map
translation tables, the band_conditions Kp/SFI -> Good/Fair/Poor
heuristic, anchor-priority ordering, expires-bucket boundaries, the
NOAA G/R/S scale tables. None of these reach the GUI.
Hot-path performance: every accessor read hits the in-memory cache after
the first call; cache hit is one dict get. Per-event reads (e.g. WFIGS
cooldown_seconds on every WFIGS poll-cycle) add a single dict lookup
to existing pipelines.
Backward-compat aliases retained for module-level imports that exist in
test code: WFIGS_BROADCAST_COOLDOWN_S, FIRMS_CONFIDENCE_FLOOR,
FIRMS_FRP_FLOOR, FIRMS_BBOX_OPTIONAL, INCIDENT_FRESHNESS_MAX_S,
PHOTON_BASE_URL/TIMEOUT_S/RADIUS_KM/LIMIT. Handler code reads via
adapter_config; tests can either monkeypatch the module attribute (firms)
or mutate adapter_config DB values.
Test count: 731 -> 731 (no new tests in 3b -- handler wiring is a pure
refactor; coverage comes from the existing handler test suites passing
unchanged).
Refs audit doc v0.6-phase1-audit.md Section A + Matt s CONFIG-vs-CODE rule.
97 lines
3.4 KiB
Python
97 lines
3.4 KiB
Python
"""Severity-based event inhibitor.
|
|
|
|
Suppresses lower-severity events when a higher-severity event for the
|
|
same logical incident (matching inhibit_keys) is already active.
|
|
|
|
Inhibit keys are operator-defined strings on the Event that identify
|
|
the underlying incident. Two events sharing an inhibit_key refer to
|
|
the same situation. If a critical event for "battery:BLD-MTN" fired
|
|
recently, a subsequent warning event with the same key gets suppressed.
|
|
"""
|
|
|
|
import logging
|
|
import time
|
|
from typing import Callable
|
|
|
|
from meshai.notifications.events import Event
|
|
|
|
|
|
class Inhibitor:
|
|
"""Suppress lower-severity events when higher-severity is active."""
|
|
|
|
SEVERITY_RANK = {"routine": 0, "priority": 1, "immediate": 2}
|
|
|
|
def __init__(
|
|
self,
|
|
next_handler: Callable[[Event], None],
|
|
ttl_seconds: float | None = None,
|
|
):
|
|
"""Initialize.
|
|
|
|
Args:
|
|
next_handler: Callable that receives non-suppressed events.
|
|
ttl_seconds: How long an inhibit_key remains active after
|
|
the originating event. None -> read from
|
|
adapter_config.pipeline.inhibitor_ttl_seconds (default
|
|
1800). v0.6-3b: explicit value still wins for tests.
|
|
"""
|
|
self._next = next_handler
|
|
if ttl_seconds is None:
|
|
from meshai.adapter_config import adapter_config
|
|
ttl_seconds = float(adapter_config.pipeline.inhibitor_ttl_seconds)
|
|
self._ttl = ttl_seconds
|
|
# {inhibit_key: (rank, expires_at)}
|
|
self._active: dict[str, tuple[int, float]] = {}
|
|
self._logger = logging.getLogger("meshai.pipeline.inhibitor")
|
|
|
|
def _now(self) -> float:
|
|
# Hookable for tests
|
|
return time.time()
|
|
|
|
def _prune_expired(self, now: float) -> None:
|
|
expired = [k for k, (_, exp) in self._active.items() if exp <= now]
|
|
for k in expired:
|
|
del self._active[k]
|
|
|
|
def handle(self, event: Event) -> None:
|
|
"""Process an event: either suppress it or pass it on.
|
|
|
|
If any of the event's inhibit_keys is currently active at a
|
|
higher-or-equal rank, the event is suppressed. Otherwise, the
|
|
event's inhibit_keys are recorded/upgraded, and the event is
|
|
passed to the next handler.
|
|
"""
|
|
now = self._now()
|
|
self._prune_expired(now)
|
|
|
|
event_rank = self.SEVERITY_RANK.get(event.severity, 0)
|
|
|
|
# Check suppression
|
|
for key in event.inhibit_keys:
|
|
entry = self._active.get(key)
|
|
if entry is not None:
|
|
active_rank, _ = entry
|
|
if active_rank >= event_rank:
|
|
self._logger.info(
|
|
f"SUPPRESSED event {event.id} ({event.severity}) "
|
|
f"by active key {key!r} at rank {active_rank}"
|
|
)
|
|
return
|
|
|
|
# Record / upgrade entries
|
|
new_expires = now + self._ttl
|
|
for key in event.inhibit_keys:
|
|
existing = self._active.get(key)
|
|
if existing is None or existing[0] < event_rank:
|
|
self._active[key] = (event_rank, new_expires)
|
|
|
|
# Pass through
|
|
self._next(event)
|
|
|
|
def active_keys(self) -> dict[str, tuple[int, float]]:
|
|
"""For tests: snapshot of currently-active inhibit keys."""
|
|
return dict(self._active)
|
|
|
|
def clear(self) -> None:
|
|
"""For tests: reset state."""
|
|
self._active.clear()
|