mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-05-21 15:14:45 +02:00
fix: 6 reporter bugs — missing methods, undefined var, indentation, key types
1. Added _find_node() — delegates to health_engine.get_node() 2. Added _find_region() — fuzzy match with config aliases 3. Fixed undefined unified var in _node_recommendations 4. Fixed env recommendation indentation (was inside MQTT uplink check) 5. Fixed 6 string-vs-int key mismatches on health.nodes lookups 6. Fixed estimated_position_interval — compute inline from packets_by_type Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
cc474e3bb3
commit
29a57b459a
1 changed files with 104 additions and 41 deletions
|
|
@ -177,6 +177,52 @@ class MeshReporter:
|
||||||
return local or desc
|
return local or desc
|
||||||
|
|
||||||
|
|
||||||
|
def _find_node(self, identifier: str) -> "UnifiedNode | None":
|
||||||
|
"""Find a node by any identifier (shortname, longname, node_num, hex ID).
|
||||||
|
|
||||||
|
Delegates to health_engine.get_node() which searches all formats.
|
||||||
|
"""
|
||||||
|
return self.health_engine.get_node(identifier)
|
||||||
|
|
||||||
|
def _find_region(self, region_name: str) -> "RegionHealth | None":
|
||||||
|
"""Find a region by name (fuzzy match).
|
||||||
|
|
||||||
|
Tries exact match, then substring, then alias from config.
|
||||||
|
"""
|
||||||
|
health = self.health_engine.mesh_health
|
||||||
|
if not health:
|
||||||
|
return None
|
||||||
|
|
||||||
|
name_lower = region_name.lower().strip()
|
||||||
|
|
||||||
|
# Exact match
|
||||||
|
for region in health.regions:
|
||||||
|
if region.name.lower() == name_lower:
|
||||||
|
return region
|
||||||
|
|
||||||
|
# Substring match (longest region name first to avoid partial matches)
|
||||||
|
for region in sorted(health.regions, key=lambda r: len(r.name), reverse=True):
|
||||||
|
if name_lower in region.name.lower() or region.name.lower() in name_lower:
|
||||||
|
return region
|
||||||
|
|
||||||
|
# Check config aliases
|
||||||
|
for region_cfg_name, cfg in self._region_configs.items():
|
||||||
|
aliases = getattr(cfg, 'aliases', []) or []
|
||||||
|
cities = getattr(cfg, 'cities', []) or []
|
||||||
|
local_name = getattr(cfg, 'local_name', '') or ''
|
||||||
|
|
||||||
|
all_matches = [a.lower() for a in aliases] + [c.lower() for c in cities]
|
||||||
|
if local_name:
|
||||||
|
all_matches.append(local_name.lower())
|
||||||
|
|
||||||
|
if name_lower in all_matches or any(name_lower in m for m in all_matches):
|
||||||
|
# Found config match, now find the region
|
||||||
|
for region in health.regions:
|
||||||
|
if region.name == region_cfg_name:
|
||||||
|
return region
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def _build_source_health_section(self) -> list[str]:
|
def _build_source_health_section(self) -> list[str]:
|
||||||
"""Build source health section for Tier 1."""
|
"""Build source health section for Tier 1."""
|
||||||
lines = []
|
lines = []
|
||||||
|
|
@ -312,7 +358,7 @@ class MeshReporter:
|
||||||
single_client_nodes = [n for n in single_gw_nodes if not n.is_infrastructure]
|
single_client_nodes = [n for n in single_gw_nodes if not n.is_infrastructure]
|
||||||
lines.append(f" Single-gw clients ({single_clients}):")
|
lines.append(f" Single-gw clients ({single_clients}):")
|
||||||
for scn in single_client_nodes[:10]:
|
for scn in single_client_nodes[:10]:
|
||||||
scn_name = _node_display_name(scn.long_name, scn.short_name, str(scn.node~um))
|
scn_name = _node_display_name(scn.long_name, scn.short_name, str(scn.node_num))
|
||||||
src_info = f" via {scn.sources[0]}" if len(scn.sources) == 1 else ""
|
src_info = f" via {scn.sources[0]}" if len(scn.sources) == 1 else ""
|
||||||
lines.append(f" {scn_name}{src_info}")
|
lines.append(f" {scn_name}{src_info}")
|
||||||
if len(single_client_nodes) > 10:
|
if len(single_client_nodes) > 10:
|
||||||
|
|
@ -592,17 +638,25 @@ class MeshReporter:
|
||||||
# Infrastructure issues (offline nodes)
|
# Infrastructure issues (offline nodes)
|
||||||
for region in health.regions:
|
for region in health.regions:
|
||||||
offline_infra = []
|
offline_infra = []
|
||||||
for nid in region.node_ids:
|
for nid_str in region.node_ids:
|
||||||
|
try:
|
||||||
|
nid = int(nid_str)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
node = health.nodes.get(nid)
|
node = health.nodes.get(nid)
|
||||||
if node and node.is_infrastructure and not node.is_online:
|
if node and node.is_infrastructure and not node.is_online:
|
||||||
name = _node_display_name(node.long_name, node.short_name, nid)
|
name = _node_display_name(node.long_name, node.short_name, str(nid))
|
||||||
offline_infra.append(name)
|
offline_infra.append(name)
|
||||||
if offline_infra:
|
if offline_infra:
|
||||||
total_infra = sum(
|
total_infra = 0
|
||||||
1
|
for nid_str in region.node_ids:
|
||||||
for nid in region.node_ids
|
try:
|
||||||
if health.nodes.get(nid) and health.nodes[nid].is_infrastructure
|
nid = int(nid_str)
|
||||||
)
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
node = health.nodes.get(nid)
|
||||||
|
if node and node.is_infrastructure:
|
||||||
|
total_infra += 1
|
||||||
issues.append(
|
issues.append(
|
||||||
f"{region.name}: {len(offline_infra)}/{total_infra} infrastructure offline "
|
f"{region.name}: {len(offline_infra)}/{total_infra} infrastructure offline "
|
||||||
f"({', '.join(offline_infra[:3])})"
|
f"({', '.join(offline_infra[:3])})"
|
||||||
|
|
@ -717,11 +771,15 @@ class MeshReporter:
|
||||||
lines.append("Channel Utilization: No data available")
|
lines.append("Channel Utilization: No data available")
|
||||||
|
|
||||||
# MQTT uplink stats for region
|
# MQTT uplink stats for region
|
||||||
uplink_nodes = [
|
uplink_nodes = []
|
||||||
health.nodes.get(nid)
|
for nid_str in region.node_ids:
|
||||||
for nid in region.node_ids
|
try:
|
||||||
if health.nodes.get(nid) and health.nodes[nid].uplink_enabled
|
nid = int(nid_str)
|
||||||
]
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
node = health.nodes.get(nid)
|
||||||
|
if node and node.uplink_enabled:
|
||||||
|
uplink_nodes.append(node)
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(f"MQTT Uplinks: {len(uplink_nodes)} nodes")
|
lines.append(f"MQTT Uplinks: {len(uplink_nodes)} nodes")
|
||||||
|
|
||||||
|
|
@ -1085,7 +1143,7 @@ class MeshReporter:
|
||||||
|
|
||||||
# Check if trending up
|
# Check if trending up
|
||||||
trend_note = ""
|
trend_note = ""
|
||||||
if unified:
|
if node:
|
||||||
avg_7d = node.packets_sent_7d / 7 if node.packets_sent_7d else 0
|
avg_7d = node.packets_sent_7d / 7 if node.packets_sent_7d else 0
|
||||||
if avg_7d > 0 and node.packets_sent_24h > avg_7d * 1.5:
|
if avg_7d > 0 and node.packets_sent_24h > avg_7d * 1.5:
|
||||||
trend_note = " (trending up vs 7d avg)"
|
trend_note = " (trending up vs 7d avg)"
|
||||||
|
|
@ -1144,30 +1202,30 @@ class MeshReporter:
|
||||||
)
|
)
|
||||||
|
|
||||||
# Environmental recommendations
|
# Environmental recommendations
|
||||||
# Freezing temperature warning for battery nodes
|
# Freezing temperature warning for battery nodes
|
||||||
if node.temperature is not None and node.temperature < 0:
|
if node.temperature is not None and node.temperature < 0:
|
||||||
if node.battery_percent is not None and node.battery_percent <= 100:
|
if node.battery_percent is not None and node.battery_percent <= 100:
|
||||||
recs.append(
|
|
||||||
f"Temperature {node.temperature:.1f}C - below freezing reduces battery capacity 20-40%."
|
|
||||||
)
|
|
||||||
|
|
||||||
# High humidity condensation risk
|
|
||||||
if node.humidity is not None and node.humidity > 90:
|
|
||||||
recs.append(
|
recs.append(
|
||||||
f"Humidity at {node.humidity:.0f}% - condensation risk. Ensure enclosure is sealed."
|
f"Temperature {node.temperature:.1f}C - below freezing reduces battery capacity 20-40%."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Poor air quality
|
# High humidity condensation risk
|
||||||
if node.pm2_5 is not None and node.pm2_5 > 35:
|
if node.humidity is not None and node.humidity > 90:
|
||||||
recs.append(
|
recs.append(
|
||||||
f"PM2.5 at {node.pm2_5:.1f} ug/m3 - unhealthy air quality in this area."
|
f"Humidity at {node.humidity:.0f}% - condensation risk. Ensure enclosure is sealed."
|
||||||
)
|
)
|
||||||
|
|
||||||
# High wind
|
# Poor air quality
|
||||||
if node.wind_speed is not None and node.wind_speed > 20:
|
if node.pm2_5 is not None and node.pm2_5 > 35:
|
||||||
recs.append(
|
recs.append(
|
||||||
f"Wind speed {node.wind_speed:.1f} m/s - check antenna mounting and cable strain relief."
|
f"PM2.5 at {node.pm2_5:.1f} ug/m3 - unhealthy air quality in this area."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# High wind
|
||||||
|
if node.wind_speed is not None and node.wind_speed > 20:
|
||||||
|
recs.append(
|
||||||
|
f"Wind speed {node.wind_speed:.1f} m/s - check antenna mounting and cable strain relief."
|
||||||
|
)
|
||||||
|
|
||||||
return recs
|
return recs
|
||||||
|
|
||||||
|
|
@ -1245,11 +1303,12 @@ class MeshReporter:
|
||||||
for nid_str in region.node_ids:
|
for nid_str in region.node_ids:
|
||||||
try:
|
try:
|
||||||
nid = int(nid_str)
|
nid = int(nid_str)
|
||||||
except ValueError:
|
except (ValueError, TypeError):
|
||||||
continue
|
continue
|
||||||
node = health.nodes.get(nid)
|
node = health.nodes.get(nid)
|
||||||
if node:
|
if node:
|
||||||
est = node.estimated_position_interval
|
pos_count = node.packets_by_type.get("POSITION_APP", 0)
|
||||||
|
est = 86400 / pos_count if pos_count > 0 else None
|
||||||
if est is not None and est < 300:
|
if est is not None and est < 300:
|
||||||
aggressive_interval_nodes.append(node)
|
aggressive_interval_nodes.append(node)
|
||||||
if aggressive_interval_nodes:
|
if aggressive_interval_nodes:
|
||||||
|
|
@ -1262,11 +1321,15 @@ class MeshReporter:
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check MQTT/uplink coverage in region
|
# Check MQTT/uplink coverage in region
|
||||||
infra_nodes = [
|
infra_nodes = []
|
||||||
health.nodes.get(nid)
|
for nid_str in region.node_ids:
|
||||||
for nid in region.node_ids
|
try:
|
||||||
if health.nodes.get(nid) and health.nodes[nid].is_infrastructure
|
nid = int(nid_str)
|
||||||
]
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
node = health.nodes.get(nid)
|
||||||
|
if node and node.is_infrastructure:
|
||||||
|
infra_nodes.append(node)
|
||||||
uplink_count = sum(1 for n in infra_nodes if n and n.uplink_enabled)
|
uplink_count = sum(1 for n in infra_nodes if n and n.uplink_enabled)
|
||||||
if infra_nodes and uplink_count == 0:
|
if infra_nodes and uplink_count == 0:
|
||||||
recs.append(
|
recs.append(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue