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)))
|
||||
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:
|
||||
# Get recent packets from this node
|
||||
packets = source.fetch_recent_packets(node.node_num, limit=5)
|
||||
|
|
@ -1986,6 +1991,67 @@ class MeshDataStore:
|
|||
|
||||
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(
|
||||
self, node_num: int, metric: str, window: str = "24h"
|
||||
) -> list[tuple[float, float]]:
|
||||
|
|
@ -2023,6 +2089,23 @@ class MeshDataStore:
|
|||
"""Expose database connection for reporter queries."""
|
||||
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:
|
||||
"""Close database connection."""
|
||||
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