- system_config.normalize(): a P25 system with config.mode "conventional"
carries config.channels [{freq MHz, name, nac?}] (1-2, within 1.5 MHz) and
can no longer carry control channels (422). A missing mode is trunked and
passes through untouched, so there is no migration. An analog channel is
mirrored into control_channels for older nodes.
- talkgroups.name_from_system resolves the edge node's synthetic per-channel
id (freq in 100 Hz units) to the channel name.
- The checkin persists radio_channels (conventional channels + encryption).
- Systems editor: Type = P25 trunked / P25 conventional / Analog (NBFM) /
DMR, plus a channel-row editor for conventional and a one-frequency form for
analog. The node page shows conventional channels and an encryption notice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
81 lines
2.9 KiB
Python
81 lines
2.9 KiB
Python
"""
|
|
Talkgroup name resolution.
|
|
|
|
C2 owns the `systems` collection, and a system's config carries the full
|
|
talkgroup table — id and human name for every channel the node scans. The edge
|
|
node only knows the name when OP25 happened to have it in the loaded tags file,
|
|
so `tgid_name` on a call_start, and the `talkgroup_name` form field on /upload,
|
|
are both frequently empty for a talkgroup C2 can name perfectly well.
|
|
|
|
This resolver is the single place that closes that gap. `mqtt_handler` had its
|
|
own copy of the lookup on the call_start path, so calls got a name written to
|
|
their document while the /upload path — the one that drives transcription,
|
|
correlation and, critically, the incident *title* — kept whatever empty string
|
|
the node sent. The result was 84 of 100 incidents named "Ems — TGID 9048"
|
|
instead of "Ems — Ossining Police Dispatch" (server-26#34).
|
|
|
|
Order of preference: whatever the caller was given, then the call document
|
|
(written at call_start), then the system config. Returns None when nothing
|
|
knows the name, so callers keep their existing "TGID {id}" fallback.
|
|
"""
|
|
|
|
from typing import Optional
|
|
|
|
from app.internal import firestore as fstore
|
|
from app.internal.logger import logger
|
|
|
|
|
|
async def name_from_system(system_id: Optional[str], talkgroup_id: Optional[int]) -> Optional[str]:
|
|
"""Look a talkgroup's name up in its system's config. None if unknown."""
|
|
if not system_id or talkgroup_id is None:
|
|
return None
|
|
try:
|
|
tgid_int = int(talkgroup_id)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
system_doc = await fstore.doc_get_cached("systems", system_id)
|
|
if not system_doc:
|
|
return None
|
|
|
|
for tg in system_doc.get("config", {}).get("talkgroups", []):
|
|
try:
|
|
if int(tg.get("id", -1)) == tgid_int:
|
|
return tg.get("name") or None
|
|
except (TypeError, ValueError):
|
|
continue
|
|
# A conventional/analog call labelled with its channel's synthetic id
|
|
# (frequency in 100 Hz units, node-26#24) is named after the channel.
|
|
from app.internal.system_config import conventional_channel_name
|
|
return conventional_channel_name(system_doc, tgid_int)
|
|
|
|
|
|
async def resolve(
|
|
system_id: Optional[str],
|
|
talkgroup_id: Optional[int],
|
|
hint: Optional[str] = None,
|
|
call_doc: Optional[dict] = None,
|
|
) -> Optional[str]:
|
|
"""
|
|
Best available human name for a talkgroup.
|
|
|
|
`hint` is whatever the caller already had (OP25 metadata, a form field).
|
|
`call_doc` is an already-fetched call document, if the caller has one —
|
|
passing it avoids a second read.
|
|
"""
|
|
if hint:
|
|
return hint
|
|
|
|
if call_doc:
|
|
from_doc = call_doc.get("talkgroup_name")
|
|
if from_doc:
|
|
return from_doc
|
|
|
|
resolved = await name_from_system(system_id, talkgroup_id)
|
|
if resolved:
|
|
logger.info(
|
|
f"Resolved talkgroup name from system config: "
|
|
f"TGID {talkgroup_id} → {resolved!r}"
|
|
)
|
|
return resolved
|