mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-05-21 23:24:44 +02:00
Samples recent packets and calls /api/packets_seen to discover which physical MQTT gateways hear each node. Per-gateway RSSI and SNR. UnifiedNode: - feeder_gateways list with gateway_id, gateway_name, avg_rssi, avg_snr - feeder_count, feeder_best (strongest signal), feeder_worst MeshviewSource: - Added feeders to ENDPOINT_SCHEDULE (every 20 ticks / 10 min) - _fetch_feeders() samples 20 packets and queries packets_seen - Auto-disables if endpoint returns 404 MeshDataStore: - _enrich_feeder_data() aggregates gateway data across all sources - _normalize_node_id() helper for hex/decimal conversion - get_feeder_map() shows per-gateway coverage statistics - get_node_feeders() returns sorted gateway list for a node MeshReporter: - Node detail shows feeder gateways with signal strength - Tier 1 shows total unique gateways and avg per node Discovered gateways: AIDA, BKBS, STLR, N7MH, stor, JTS Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
255 lines
7.9 KiB
Python
255 lines
7.9 KiB
Python
"""Unified data models for the mesh data pipeline.
|
|
|
|
These dataclasses represent the normalized, merged data model used by
|
|
consumers (health engine, reporter, commands). All field normalization
|
|
happens in MeshDataStore before populating these models.
|
|
"""
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Optional
|
|
|
|
|
|
@dataclass
|
|
class UnifiedNode:
|
|
"""Unified node representation with normalized fields.
|
|
|
|
Keyed by node_num (canonical Meshtastic node number).
|
|
All fields have sensible defaults.
|
|
"""
|
|
|
|
# Identity
|
|
node_num: int
|
|
node_id_hex: str = "" # "!a3145a04"
|
|
short_name: str = ""
|
|
long_name: str = ""
|
|
role: str = "UNKNOWN" # ROUTER, CLIENT, etc.
|
|
hw_model: str = ""
|
|
|
|
# Position
|
|
latitude: Optional[float] = None
|
|
longitude: Optional[float] = None
|
|
altitude: Optional[float] = None
|
|
|
|
# Status (current)
|
|
last_heard: float = 0.0 # Epoch seconds
|
|
is_online: bool = False
|
|
hops_away: Optional[int] = None
|
|
snr: Optional[float] = None
|
|
rssi: Optional[int] = None
|
|
|
|
# Power (current)
|
|
battery_percent: Optional[float] = None
|
|
voltage: Optional[float] = None
|
|
|
|
# Power (trend - computed from historical store)
|
|
battery_trend: Optional[str] = None # "charging", "stable", "declining"
|
|
predicted_depletion_hours: Optional[float] = None
|
|
has_solar: bool = False
|
|
|
|
# Environment (from sensors - BME280, BME680, SHT31, etc.)
|
|
temperature: Optional[float] = None # Celsius
|
|
humidity: Optional[float] = None # Relative humidity %
|
|
barometric_pressure: Optional[float] = None # hPa
|
|
gas_resistance: Optional[float] = None # Ohms (AQI proxy)
|
|
iaq: Optional[float] = None # Indoor Air Quality index
|
|
|
|
# Light (BH1750, TSL2591, etc.)
|
|
light_lux: Optional[float] = None # lux
|
|
|
|
# Wind/Weather (DFROBOT_LARK stations)
|
|
wind_speed: Optional[float] = None # m/s
|
|
wind_direction: Optional[float] = None # degrees
|
|
rainfall: Optional[float] = None # mm
|
|
|
|
# Air Quality (PMSA003I)
|
|
pm1_0: Optional[float] = None # µg/m³
|
|
pm2_5: Optional[float] = None # µg/m³
|
|
pm10: Optional[float] = None # µg/m³
|
|
|
|
# Power monitoring (INA sensors - separate from battery)
|
|
ext_voltage: Optional[float] = None # External voltage (e.g., solar panel)
|
|
ext_current: Optional[float] = None # External current (mA)
|
|
|
|
# Health (MAX30102, MLX - rare)
|
|
heart_rate: Optional[float] = None # BPM
|
|
spo2: Optional[float] = None # Oxygen saturation %
|
|
body_temperature: Optional[float] = None # Celsius
|
|
|
|
# Radiation (RadSens)
|
|
radiation_cpm: Optional[float] = None # Counts per minute
|
|
|
|
# UV (VEML6070, etc.)
|
|
uv_index: Optional[float] = None
|
|
|
|
# Sensor type flags (set after populating fields)
|
|
has_environment_sensor: bool = False
|
|
has_air_quality_sensor: bool = False
|
|
has_power_sensor: bool = False
|
|
has_health_sensor: bool = False
|
|
has_weather_station: bool = False
|
|
|
|
# Traffic (current, from most recent API data)
|
|
packets_sent_24h: int = 0
|
|
packets_seen_24h: int = 0
|
|
packets_by_type: dict[str, int] = field(default_factory=dict)
|
|
text_messages_24h: int = 0
|
|
|
|
# Traffic (historical, from SQLite)
|
|
packets_sent_48h: int = 0
|
|
packets_sent_7d: int = 0
|
|
packets_sent_14d: int = 0
|
|
daily_packet_counts: dict[str, int] = field(default_factory=dict)
|
|
|
|
# Device-reported metrics (current)
|
|
channel_utilization: Optional[float] = None
|
|
air_util_tx: Optional[float] = None
|
|
uptime_seconds: Optional[int] = None
|
|
|
|
# Connectivity
|
|
uplink_enabled: bool = False
|
|
neighbors: list[int] = field(default_factory=list)
|
|
neighbor_count: int = 0
|
|
traceroute_appearances: int = 0
|
|
|
|
# Metadata
|
|
sources: list[str] = field(default_factory=list)
|
|
region: str = "" # Set by health engine
|
|
locality: str = "" # Set by health engine
|
|
|
|
# Deliverability / Coverage (from Meshview gateway counts)
|
|
avg_gateways: Optional[float] = None # Avg unique gateways that hear this node's packets
|
|
|
|
# Feeder-level gateway awareness (from /api/packets_seen across sources)
|
|
feeder_gateways: list = field(default_factory=list)
|
|
# Each entry: {"gateway_id": str, "gateway_name": str, "avg_rssi": float, "avg_snr": float, "packet_count": int}
|
|
feeder_count: int = 0 # Number of unique physical gateways that hear this node
|
|
feeder_best: Optional[str] = None # Gateway with strongest signal to this node
|
|
feeder_worst: Optional[str] = None # Gateway with weakest signal
|
|
deliverability_score: Optional[float] = None # % of packets reaching 2+ gateways (0-100)
|
|
max_gateways: Optional[int] = None # Max gateways any single packet reached
|
|
source_reach: Optional[float] = None # Avg number of Meshview sources that see this node's packets
|
|
|
|
# Additional MeshMonitor fields
|
|
firmware_version: str = ""
|
|
public_key: str = ""
|
|
is_mqtt_gateway: bool = False
|
|
via_mqtt: bool = False
|
|
|
|
# Health scoring (set by MeshHealthEngine)
|
|
is_infrastructure: bool = False
|
|
health_score: float = 100.0
|
|
infra_score: float = 100.0
|
|
util_score: float = 100.0
|
|
coverage_score_node: float = 100.0
|
|
behavior_score: float = 100.0
|
|
power_score: float = 100.0
|
|
|
|
|
|
@dataclass
|
|
class UnifiedEdge:
|
|
"""Unified edge (link) between two nodes."""
|
|
|
|
from_node: int
|
|
to_node: int
|
|
snr: Optional[float] = None
|
|
rssi: Optional[int] = None
|
|
quality: Optional[float] = None
|
|
last_seen: float = 0.0
|
|
sources: list[str] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class UnifiedTraceroute:
|
|
"""Unified traceroute record."""
|
|
|
|
from_node: int
|
|
to_node: int
|
|
route: list[int] = field(default_factory=list) # Node nums in path
|
|
route_back: list[int] = field(default_factory=list)
|
|
snr_towards: list[float] = field(default_factory=list)
|
|
snr_back: list[float] = field(default_factory=list)
|
|
timestamp: float = 0.0
|
|
request_id: int = 0
|
|
sources: list[str] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class UnifiedChannel:
|
|
"""Unified channel configuration."""
|
|
|
|
channel_id: int
|
|
name: str = ""
|
|
role: int = 0 # 0=disabled, 1=primary, 2=secondary
|
|
role_name: str = ""
|
|
uplink_enabled: bool = False
|
|
downlink_enabled: bool = False
|
|
position_precision: int = 0
|
|
sources: list[str] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class UnifiedSolar:
|
|
"""Solar estimate for a node."""
|
|
|
|
node_num: int
|
|
estimate_watts: Optional[float] = None
|
|
confidence: Optional[float] = None
|
|
timestamp: float = 0.0
|
|
sources: list[str] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class DailyTraffic:
|
|
"""Per-day aggregate traffic counts."""
|
|
|
|
date: str # "2026-05-04"
|
|
total_packets: int = 0
|
|
packets_by_type: dict[str, int] = field(default_factory=dict)
|
|
source: str = ""
|
|
|
|
|
|
@dataclass
|
|
class NodeSnapshot:
|
|
"""Point-in-time snapshot of a node's metrics."""
|
|
|
|
timestamp: float
|
|
node_num: int
|
|
short_name: str = ""
|
|
long_name: str = ""
|
|
role: str = ""
|
|
hw_model: str = ""
|
|
latitude: Optional[float] = None
|
|
longitude: Optional[float] = None
|
|
last_heard: float = 0.0
|
|
is_online: bool = False
|
|
battery_percent: Optional[float] = None
|
|
voltage: Optional[float] = None
|
|
packets_sent_24h: int = 0
|
|
packets_seen_24h: int = 0
|
|
channel_utilization: Optional[float] = None
|
|
air_util_tx: Optional[float] = None
|
|
uplink_enabled: bool = False
|
|
neighbor_count: int = 0
|
|
hops_away: Optional[int] = None
|
|
snr: Optional[float] = None
|
|
sources: str = ""
|
|
|
|
|
|
@dataclass
|
|
class MeshSnapshot:
|
|
"""Point-in-time snapshot of mesh-wide metrics."""
|
|
|
|
timestamp: float
|
|
total_nodes: int = 0
|
|
active_nodes: int = 0
|
|
infra_online: int = 0
|
|
infra_total: int = 0
|
|
total_packets_24h: int = 0
|
|
avg_channel_utilization: Optional[float] = None
|
|
avg_battery_percent: Optional[float] = None
|
|
source_count: int = 0
|
|
# Deliverability metrics
|
|
avg_gateways_mesh: Optional[float] = None
|
|
total_packets_global: Optional[int] = None
|
|
total_seen_global: Optional[int] = None
|
|
unique_feeders: Optional[int] = None
|