"""State integration tests for AIS-catcher binary telemetry.""" from __future__ import annotations from ais_hub.core.bus import EventBus from ais_hub.core.state import State from ais_hub.core.stats import Stats from ais_hub.parser.aiscatcher import ( RssiIq, SignalEvent, SignalEventBatch, SlotBitmap, SlotDetail, SlotLevel, ) def _make_state() -> State: stats = Stats() bus = EventBus(stats=stats, default_maxsize=64) return State(bus=bus, stats=stats) def test_rssi_iq_updates_state_and_publishes(): state = _make_state() sub = state._bus.subscribe(maxsize=16) state.apply_rssi_iq(RssiIq(power_a_db=-41.5, power_b_db=-43.0), ts=123.0) assert state.radio_power == { "ts": 123.0, "power_a_db": -41.5, "power_b_db": -43.0, } assert not sub.empty() ev = sub.get_nowait() assert ev.type == "radio.update" assert ev.data["source"] == "aiscatcher_rssi" assert ev.data["power_a_db"] == -41.5 def test_slot_bitmap_stored_per_channel(): state = _make_state() snap = SlotBitmap( channel="A", utc_minute=28279310, slots_total=2250, occupied_count=100, noise_floor=0.0, threshold=0.0, slot0_unix_ms=28279310 * 60000, first_occupied_unix_ms=0, bitmap=bytes(282), ) state.apply_slot_bitmap(snap, ts=50.0) assert "A" in state.slot_occupancy occ = state.slot_occupancy["A"] assert occ["occupied_count"] == 100 assert occ["slots_total"] == 2250 assert occ["occupied_fraction"] == 100 / 2250 # snapshot_slots exposes everything for REST snap_dict = state.snapshot_slots() assert snap_dict["occupancy"]["A"]["occupied_count"] == 100 def test_slot_detail_stores_entries(): state = _make_state() detail = SlotDetail( channel="B", utc_minute=1, slot0_unix_ms=60000, entries=[SlotLevel(slot=10, level_db=-70.0), SlotLevel(slot=2200, level_db=-80.0)], ) state.apply_slot_detail(detail, ts=60.0) stored = state.slot_detail["B"] assert len(stored["entries"]) == 2 assert stored["entries"][0] == {"slot": 10, "level_db": -70.0} def test_signal_event_creates_target_with_signal_fields(): state = _make_state() batch = SignalEventBatch( channel="A", events=[ SignalEvent(unix_ms=1_700_000_000_000, slot=42, mmsi=257_000_001, level_db=-75.0), ], ) state.apply_signal_events(batch, ts=1_700_000_000.0) assert 257_000_001 in state.targets tgt = state.targets[257_000_001] assert tgt.last_signal_db == -75.0 assert tgt.last_signal_slot == 42 assert tgt.last_signal_channel == "A" # last_seen must be set from signal event if it's newer assert tgt.last_seen >= 1_700_000_000.0 # to_dict exposes signal block d = tgt.to_dict() assert d["signal"] == { "last_db": -75.0, "last_ts": 1_700_000_000.0, "last_slot": 42, "last_channel": "A", } def test_signal_event_older_does_not_overwrite(): state = _make_state() mmsi = 111_222_333 # first, newer event state.apply_signal_events(SignalEventBatch( channel="A", events=[SignalEvent(unix_ms=2_000_000_000_000, slot=1, mmsi=mmsi, level_db=-60.0)], ), ts=2_000_000_000.0) # then, older event — must not clobber last_signal_db state.apply_signal_events(SignalEventBatch( channel="B", events=[SignalEvent(unix_ms=1_000_000_000_000, slot=99, mmsi=mmsi, level_db=-90.0)], ), ts=2_000_000_001.0) tgt = state.targets[mmsi] assert tgt.last_signal_db == -60.0 assert tgt.last_signal_slot == 1 assert tgt.last_signal_channel == "A"