mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-05-21 23:24:44 +02:00
fix: Add backwards compatibility methods for mesh_health.py
- Add hasattr check for fetch_recent_packets in gateway sampling - Add get_all_nodes(), get_all_telemetry(), get_all_packets(), get_all_edges() methods - These methods return data in dict format expected by mesh_health.py Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
af2f66d71d
commit
f30cd0a8bf
2 changed files with 322 additions and 0 deletions
|
|
@ -1265,6 +1265,11 @@ class MeshDataStore:
|
||||||
sample_nodes = random.sample(infra_nodes, min(5, len(infra_nodes)))
|
sample_nodes = random.sample(infra_nodes, min(5, len(infra_nodes)))
|
||||||
sampled_count = 0
|
sampled_count = 0
|
||||||
|
|
||||||
|
# Check if source supports required methods
|
||||||
|
if not hasattr(source, "fetch_recent_packets"):
|
||||||
|
logger.debug("Gateway sampling skipped: source lacks fetch_recent_packets")
|
||||||
|
return
|
||||||
|
|
||||||
for node in sample_nodes:
|
for node in sample_nodes:
|
||||||
# Get recent packets from this node
|
# Get recent packets from this node
|
||||||
packets = source.fetch_recent_packets(node.node_num, limit=5)
|
packets = source.fetch_recent_packets(node.node_num, limit=5)
|
||||||
|
|
@ -1986,6 +1991,67 @@ class MeshDataStore:
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
# === Backwards compatibility methods for mesh_health.py ===
|
||||||
|
|
||||||
|
def get_all_nodes(self) -> list[dict]:
|
||||||
|
"""Get all nodes as list of dicts (backwards compatibility).
|
||||||
|
|
||||||
|
Returns nodes in the format expected by mesh_health.py.
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
for node_num, node in self._nodes.items():
|
||||||
|
node_dict = {
|
||||||
|
"_node_num": node_num,
|
||||||
|
"nodeNum": node_num,
|
||||||
|
"id": node.node_id_hex,
|
||||||
|
"shortName": node.short_name,
|
||||||
|
"short_name": node.short_name,
|
||||||
|
"longName": node.long_name,
|
||||||
|
"long_name": node.long_name,
|
||||||
|
"role": node.role,
|
||||||
|
"hwModel": node.hw_model,
|
||||||
|
"latitude": node.latitude,
|
||||||
|
"longitude": node.longitude,
|
||||||
|
"lat": node.latitude,
|
||||||
|
"lon": node.longitude,
|
||||||
|
"lastHeard": node.last_heard,
|
||||||
|
"last_seen": node.last_heard,
|
||||||
|
"batteryLevel": node.battery_percent,
|
||||||
|
"battery_percent": node.battery_percent,
|
||||||
|
"voltage": node.voltage,
|
||||||
|
"channelUtilization": node.channel_utilization,
|
||||||
|
"airUtilTx": node.air_util_tx,
|
||||||
|
"uptime": node.uptime_seconds,
|
||||||
|
"mqtt_gateway": node.is_mqtt_gateway,
|
||||||
|
"_sources": list(node.sources),
|
||||||
|
}
|
||||||
|
result.append(node_dict)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_all_telemetry(self) -> list[dict]:
|
||||||
|
"""Get all telemetry records (backwards compatibility).
|
||||||
|
|
||||||
|
Returns telemetry in the format expected by mesh_health.py.
|
||||||
|
"""
|
||||||
|
# Return telemetry from all sources
|
||||||
|
result = []
|
||||||
|
for source in self._sources.values():
|
||||||
|
if hasattr(source, "telemetry"):
|
||||||
|
result.extend(source.telemetry)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_all_packets(self) -> list[dict]:
|
||||||
|
"""Get all packets (backwards compatibility).
|
||||||
|
|
||||||
|
Returns packets in the format expected by mesh_health.py.
|
||||||
|
"""
|
||||||
|
# Return packets from all sources
|
||||||
|
result = []
|
||||||
|
for source in self._sources.values():
|
||||||
|
if hasattr(source, "packets"):
|
||||||
|
result.extend(source.packets)
|
||||||
|
return result
|
||||||
|
|
||||||
def get_environment_history(
|
def get_environment_history(
|
||||||
self, node_num: int, metric: str, window: str = "24h"
|
self, node_num: int, metric: str, window: str = "24h"
|
||||||
) -> list[tuple[float, float]]:
|
) -> list[tuple[float, float]]:
|
||||||
|
|
@ -2023,6 +2089,23 @@ class MeshDataStore:
|
||||||
"""Expose database connection for reporter queries."""
|
"""Expose database connection for reporter queries."""
|
||||||
return self._db
|
return self._db
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_edges(self) -> list[dict]:
|
||||||
|
"""Get all edges as list of dicts (backwards compatibility).
|
||||||
|
|
||||||
|
Returns edges in the format expected by mesh_health.py.
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
for edge in self._edges:
|
||||||
|
edge_dict = {
|
||||||
|
"from_node": edge.from_node,
|
||||||
|
"to_node": edge.to_node,
|
||||||
|
"snr": edge.snr,
|
||||||
|
"rssi": edge.rssi,
|
||||||
|
"timestamp": edge.timestamp,
|
||||||
|
}
|
||||||
|
result.append(edge_dict)
|
||||||
|
return result
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
"""Close database connection."""
|
"""Close database connection."""
|
||||||
if self._db:
|
if self._db:
|
||||||
|
|
|
||||||
239
meshai/mesh_models.py
Normal file
239
meshai/mesh_models.py
Normal file
|
|
@ -0,0 +1,239 @@
|
||||||
|
"""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
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue