diff --git a/src/meshcore_hub/collector/handlers/contacts.py b/src/meshcore_hub/collector/handlers/contacts.py index 096fb3e..7bfff62 100644 --- a/src/meshcore_hub/collector/handlers/contacts.py +++ b/src/meshcore_hub/collector/handlers/contacts.py @@ -11,6 +11,14 @@ from meshcore_hub.common.models import Node logger = logging.getLogger(__name__) +# Map numeric node type to string representation +NODE_TYPE_MAP = { + 0: "none", + 1: "chat", + 2: "repeater", + 3: "room", +} + def handle_contacts( public_key: str, @@ -25,7 +33,7 @@ def handle_contacts( Args: public_key: Receiver node's public key (from MQTT topic) event_type: Event type name - payload: Contacts payload + payload: Contacts payload (array of contact objects from device) db: Database manager """ contacts = payload.get("contacts", []) @@ -43,16 +51,24 @@ def handle_contacts( if not contact_key: continue - name = contact.get("name") - node_type = contact.get("node_type") + # Device uses 'adv_name' for the advertised name + name = contact.get("adv_name") or contact.get("name") + + # Device uses numeric 'type' field, convert to string + raw_type = contact.get("type") + if raw_type is not None: + node_type = NODE_TYPE_MAP.get(raw_type, str(raw_type)) + else: + node_type = contact.get("node_type") # Find or create node node_query = select(Node).where(Node.public_key == contact_key) node = session.execute(node_query).scalar_one_or_none() if node: - # Update existing node - if name and not node.name: + # Update existing node - always update name if we have one + if name and name != node.name: + logger.debug(f"Updating node {contact_key[:12]}... name: {name}") node.name = name if node_type and not node.adv_type: node.adv_type = node_type @@ -69,6 +85,7 @@ def handle_contacts( ) session.add(node) created_count += 1 + logger.debug(f"Created node {contact_key[:12]}... name: {name}") logger.info( f"Processed contacts sync: {created_count} new, {updated_count} updated" diff --git a/src/meshcore_hub/common/schemas/events.py b/src/meshcore_hub/common/schemas/events.py index a72fcd1..dca8806 100644 --- a/src/meshcore_hub/common/schemas/events.py +++ b/src/meshcore_hub/common/schemas/events.py @@ -157,7 +157,16 @@ class TelemetryResponseEvent(BaseModel): class ContactInfo(BaseModel): - """Schema for a single contact in CONTACTS event.""" + """Schema for a single contact in CONTACTS event. + + Device payload fields: + - public_key: Node's 64-char hex public key + - adv_name: Node's advertised name (device field) + - type: Numeric node type (0=none, 1=chat, 2=repeater, 3=room) + - flags: Capability flags + - last_advert: Unix timestamp of last advertisement + - adv_lat, adv_lon: GPS coordinates (if available) + """ public_key: str = Field( ..., @@ -165,14 +174,40 @@ class ContactInfo(BaseModel): max_length=64, description="Node's full public key", ) + adv_name: Optional[str] = Field( + default=None, + max_length=255, + description="Node's advertised name (from device)", + ) + type: Optional[int] = Field( + default=None, + description="Numeric node type: 0=none, 1=chat, 2=repeater, 3=room", + ) + flags: Optional[int] = Field( + default=None, + description="Capability/status flags bitmask", + ) + last_advert: Optional[int] = Field( + default=None, + description="Unix timestamp of last advertisement", + ) + adv_lat: Optional[float] = Field( + default=None, + description="GPS latitude (if available)", + ) + adv_lon: Optional[float] = Field( + default=None, + description="GPS longitude (if available)", + ) + # Legacy field names for backwards compatibility name: Optional[str] = Field( default=None, max_length=255, - description="Node name/alias", + description="Node name/alias (legacy, prefer adv_name)", ) node_type: Optional[str] = Field( default=None, - description="Node type: chat, repeater, room, none", + description="Node type string (legacy, prefer type)", )