mirror of
https://github.com/ipnet-mesh/meshcore-hub.git
synced 2026-08-12 11:52:44 +02:00
Refactor contacts to emit individual MQTT events per contact
Instead of sending all contacts in one MQTT message, the interface now splits the device's contacts response into individual 'contact' events. This is more consistent with other event patterns and makes the collector simpler. Interface changes: - Add _publish_contacts() to split contacts dict into individual events - Publish each contact as 'contact' event (not 'contacts') Collector changes: - Rename handle_contacts to handle_contact for single contact - Simplify handler to process one contact per message - Register handler for 'contact' events
This commit is contained in:
@@ -19,7 +19,7 @@ def register_all_handlers(subscriber: "Subscriber") -> None:
|
||||
)
|
||||
from meshcore_hub.collector.handlers.trace import handle_trace_data
|
||||
from meshcore_hub.collector.handlers.telemetry import handle_telemetry
|
||||
from meshcore_hub.collector.handlers.contacts import handle_contacts
|
||||
from meshcore_hub.collector.handlers.contacts import handle_contact
|
||||
from meshcore_hub.collector.handlers.event_log import handle_event_log
|
||||
|
||||
# Persisted events with specific handlers
|
||||
@@ -28,7 +28,7 @@ def register_all_handlers(subscriber: "Subscriber") -> None:
|
||||
subscriber.register_handler("channel_msg_recv", handle_channel_message)
|
||||
subscriber.register_handler("trace_data", handle_trace_data)
|
||||
subscriber.register_handler("telemetry_response", handle_telemetry)
|
||||
subscriber.register_handler("contacts", handle_contacts)
|
||||
subscriber.register_handler("contact", handle_contact) # Individual contact events
|
||||
|
||||
# Informational events (logged only)
|
||||
subscriber.register_handler("send_confirmed", handle_event_log)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Handler for contacts sync events."""
|
||||
"""Handler for contact sync events."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
@@ -20,73 +20,64 @@ NODE_TYPE_MAP = {
|
||||
}
|
||||
|
||||
|
||||
def handle_contacts(
|
||||
def handle_contact(
|
||||
public_key: str,
|
||||
event_type: str,
|
||||
payload: dict[str, Any],
|
||||
db: DatabaseManager,
|
||||
) -> None:
|
||||
"""Handle a contacts sync event.
|
||||
"""Handle a single contact event.
|
||||
|
||||
Upserts all contacts in the contacts list.
|
||||
Upserts a contact into the nodes table.
|
||||
|
||||
Args:
|
||||
public_key: Receiver node's public key (from MQTT topic)
|
||||
event_type: Event type name
|
||||
payload: Contacts payload (array of contact objects from device)
|
||||
payload: Single contact object with fields:
|
||||
- public_key: Contact's public key
|
||||
- adv_name: Advertised name
|
||||
- type: Numeric node type (0=none, 1=chat, 2=repeater, 3=room)
|
||||
db: Database manager
|
||||
"""
|
||||
contacts = payload.get("contacts", [])
|
||||
if not contacts:
|
||||
logger.debug("Empty contacts list received")
|
||||
contact_key = payload.get("public_key")
|
||||
if not contact_key:
|
||||
logger.warning("Contact event missing public_key field")
|
||||
return
|
||||
|
||||
# Device uses 'adv_name' for the advertised name
|
||||
name = payload.get("adv_name") or payload.get("name")
|
||||
|
||||
# Device uses numeric 'type' field, convert to string
|
||||
raw_type = payload.get("type")
|
||||
if raw_type is not None:
|
||||
node_type: str | None = NODE_TYPE_MAP.get(raw_type, str(raw_type))
|
||||
else:
|
||||
node_type = payload.get("node_type")
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
created_count = 0
|
||||
updated_count = 0
|
||||
|
||||
with db.session_scope() as session:
|
||||
for contact in contacts:
|
||||
contact_key = contact.get("public_key")
|
||||
if not contact_key:
|
||||
continue
|
||||
# Find or create node
|
||||
node_query = select(Node).where(Node.public_key == contact_key)
|
||||
node = session.execute(node_query).scalar_one_or_none()
|
||||
|
||||
# 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 - 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
|
||||
node.last_seen = now
|
||||
updated_count += 1
|
||||
else:
|
||||
# Create new node
|
||||
node = Node(
|
||||
public_key=contact_key,
|
||||
name=name,
|
||||
adv_type=node_type,
|
||||
first_seen=now,
|
||||
last_seen=now,
|
||||
)
|
||||
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"
|
||||
)
|
||||
if node:
|
||||
# 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
|
||||
node.last_seen = now
|
||||
logger.debug(f"Updated contact: {contact_key[:12]}... ({name})")
|
||||
else:
|
||||
# Create new node
|
||||
node = Node(
|
||||
public_key=contact_key,
|
||||
name=name,
|
||||
adv_type=node_type,
|
||||
first_seen=now,
|
||||
last_seen=now,
|
||||
)
|
||||
session.add(node)
|
||||
logger.info(f"Created node from contact: {contact_key[:12]}... ({name})")
|
||||
|
||||
@@ -117,6 +117,11 @@ class Receiver:
|
||||
# Convert event type to MQTT topic name
|
||||
event_name = event_type.value
|
||||
|
||||
# Special handling for CONTACTS: split into individual messages
|
||||
if event_type == EventType.CONTACTS:
|
||||
self._publish_contacts(payload)
|
||||
return
|
||||
|
||||
# Publish to MQTT
|
||||
self.mqtt.publish_event(
|
||||
self.device.public_key,
|
||||
@@ -129,6 +134,49 @@ class Receiver:
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to publish event to MQTT: {e}")
|
||||
|
||||
def _publish_contacts(self, payload: dict[str, Any]) -> None:
|
||||
"""Publish each contact as a separate MQTT message.
|
||||
|
||||
The device returns contacts as a dict keyed by public_key.
|
||||
We split this into individual 'contact' events for cleaner processing.
|
||||
|
||||
Args:
|
||||
payload: Dict of contacts keyed by public_key
|
||||
"""
|
||||
if not self.device.public_key:
|
||||
logger.warning("Cannot publish contacts: device public key not available")
|
||||
return
|
||||
|
||||
# Handle both formats:
|
||||
# - Dict keyed by public_key (real device)
|
||||
# - Dict with "contacts" array (mock device)
|
||||
if "contacts" in payload:
|
||||
contacts = payload["contacts"]
|
||||
else:
|
||||
contacts = list(payload.values())
|
||||
|
||||
if not contacts:
|
||||
logger.debug("Empty contacts list received")
|
||||
return
|
||||
|
||||
device_key = self.device.public_key # Capture for type narrowing
|
||||
count = 0
|
||||
for contact in contacts:
|
||||
if not isinstance(contact, dict):
|
||||
continue
|
||||
|
||||
try:
|
||||
self.mqtt.publish_event(
|
||||
device_key,
|
||||
"contact", # Use singular 'contact' for individual events
|
||||
contact,
|
||||
)
|
||||
count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to publish contact event: {e}")
|
||||
|
||||
logger.info(f"Published {count} contact events to MQTT")
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start the receiver."""
|
||||
logger.info("Starting RECEIVER mode")
|
||||
|
||||
Reference in New Issue
Block a user