Added the ability to track gateways and present them in varous pages

This commit is contained in:
pablorevilla-meshtastic
2026-02-13 14:14:46 -08:00
parent 9aacceda28
commit 685dbc9505
10 changed files with 90 additions and 5 deletions

View File

@@ -0,0 +1,27 @@
"""Add is_mqtt_gateway to node
Revision ID: 23dad03d2e42
Revises: a0c9c13e118f
Create Date: 2026-02-13 00:00:00.000000
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "23dad03d2e42"
down_revision: str | None = "a0c9c13e118f"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.add_column("node", sa.Column("is_mqtt_gateway", sa.Boolean(), nullable=True))
def downgrade() -> None:
op.drop_column("node", "is_mqtt_gateway")

View File

@@ -80,8 +80,11 @@
"last_lat": "Last Latitude",
"last_long": "Last Longitude",
"channel": "Channel",
"mqtt_gateway": "MQTT",
"last_seen": "Last Seen",
"favorite": "Favorite",
"yes": "Yes",
"no": "No",
"time_just_now": "just now",
"time_min_ago": "min ago",
@@ -96,8 +99,9 @@
"view_packet_details": "More details"
},
"map": {
"map": {
"show_routers_only": "Show Routers Only",
"show_mqtt_only": "Show MQTT Gateways Only",
"share_view": "Share This View",
"reset_filters": "Reset Filters To Defaults",
"unmapped_packets_title": "Unmapped Packets",
@@ -105,8 +109,11 @@
"channel_label": "Channel:",
"model_label": "Model:",
"role_label": "Role:",
"mqtt_gateway": "MQTT Gateway:",
"last_seen": "Last seen:",
"firmware": "Firmware:",
"yes": "Yes",
"no": "No",
"link_copied": "Link Copied!",
"legend_traceroute": "Traceroute (with arrows)",
"legend_neighbor": "Neighbor"
@@ -192,6 +199,7 @@
"hw_model": "Hardware Model",
"firmware": "Firmware",
"role": "Role",
"mqtt_gateway": "MQTT Gateway",
"channel": "Channel",
"latitude": "Latitude",
"longitude": "Longitude",
@@ -214,6 +222,8 @@
"last_24h": "24h",
"packets_sent": "Packets sent",
"times_seen": "Times seen",
"yes": "Yes",
"no": "No",
"copy_import_url": "Copy Import URL",
"show_qr_code": "Show QR Code",
"toggle_coverage": "Predicted Coverage",

View File

@@ -78,8 +78,11 @@
"last_lat": "Última latitud",
"last_long": "Última longitud",
"channel": "Canal",
"mqtt_gateway": "MQTT",
"last_seen": "Última vez visto",
"favorite": "Favorito",
"yes": "Sí",
"no": "No",
"time_just_now": "justo ahora",
"time_min_ago": "min atrás",
"time_hr_ago": "h atrás",
@@ -96,6 +99,7 @@
"map": {
"filter_routers_only": "Mostrar solo enrutadores",
"show_routers_only": "Mostrar solo enrutadores",
"show_mqtt_only": "Mostrar solo gateways MQTT",
"share_view": "Compartir esta vista",
"reset_filters": "Restablecer filtros",
"unmapped_packets_title": "Paquetes sin mapa",
@@ -103,8 +107,11 @@
"channel_label": "Canal:",
"model_label": "Modelo:",
"role_label": "Rol:",
"mqtt_gateway": "Gateway MQTT:",
"last_seen": "Visto por última vez:",
"firmware": "Firmware:",
"yes": "Sí",
"no": "No",
"link_copied": "¡Enlace copiado!",
"legend_traceroute": "Ruta de traceroute (flechas de dirección)",
"legend_neighbor": "Vínculo de vecinos"
@@ -178,6 +185,7 @@
"hw_model": "Modelo de Hardware",
"firmware": "Firmware",
"role": "Rol",
"mqtt_gateway": "Gateway MQTT",
"channel": "Canal",
"latitude": "Latitud",
"longitude": "Longitud",
@@ -200,6 +208,8 @@
"last_24h": "24h",
"packets_sent": "Paquetes enviados",
"times_seen": "Veces visto",
"yes": "Sí",
"no": "No",
"copy_import_url": "Copiar URL de importación",
"show_qr_code": "Mostrar código QR",
"toggle_coverage": "Cobertura predicha",

View File

@@ -20,6 +20,7 @@ class Node(Base):
last_lat: Mapped[int] = mapped_column(BigInteger, nullable=True)
last_long: Mapped[int] = mapped_column(BigInteger, nullable=True)
channel: Mapped[str] = mapped_column(nullable=True)
is_mqtt_gateway: Mapped[bool] = mapped_column(nullable=True)
first_seen_us: Mapped[int] = mapped_column(BigInteger, nullable=True)
last_seen_us: Mapped[int] = mapped_column(BigInteger, nullable=True)

View File

@@ -2,7 +2,7 @@ import logging
import re
import time
from sqlalchemy import select
from sqlalchemy import select, update
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
from sqlalchemy.exc import IntegrityError
@@ -15,6 +15,8 @@ from meshview.models import Node, NodePublicKey, Packet, PacketSeen, Traceroute
logger = logging.getLogger(__name__)
MQTT_GATEWAY_CACHE: set[int] = set()
async def process_envelope(topic, env):
# MAP_REPORT_APP
@@ -131,6 +133,12 @@ async def process_envelope(topic, env):
else:
node_id = int(env.gateway_id[1:], 16)
if node_id not in MQTT_GATEWAY_CACHE:
MQTT_GATEWAY_CACHE.add(node_id)
await session.execute(
update(Node).where(Node.node_id == node_id).values(is_mqtt_gateway=True)
)
result = await session.execute(
select(PacketSeen).where(
PacketSeen.packet_id == env.packet.id,
@@ -266,3 +274,11 @@ async def process_envelope(topic, env):
)
await session.commit()
async def load_gateway_cache():
async with mqtt_database.async_session() as session:
result = await session.execute(
select(Node.node_id).where(Node.is_mqtt_gateway == True) # noqa: E712
)
MQTT_GATEWAY_CACHE.update(result.scalars().all())

View File

@@ -110,6 +110,8 @@
<div id="filter-container">
<input type="checkbox" class="filter-checkbox" id="filter-routers-only">
<span data-translate-lang="show_routers_only">Show Routers Only</span>
<input type="checkbox" class="filter-checkbox" id="filter-mqtt-only">
<span data-translate-lang="show_mqtt_only">Show MQTT Gateways Only</span>
</div>
<div style="text-align:center;margin-top:5px;">
@@ -374,6 +376,7 @@ fetch('/api/nodes?days_active=3')
role: n.role || "",
firmware: n.firmware || "",
last_seen_us: n.last_seen_us || null,
is_mqtt_gateway: n.is_mqtt_gateway === true,
isRouter: (n.role||"").toLowerCase().includes("router")
}));
@@ -416,6 +419,9 @@ function renderNodesOnMap(){
<b data-translate-lang="channel_label"></b> ${node.channel}<br>
<b data-translate-lang="model_label"></b> ${node.hw_model}<br>
<b data-translate-lang="role_label"></b> ${node.role}<br>
<b data-translate-lang="mqtt_gateway"></b> ${
node.is_mqtt_gateway ? (mapTranslations.yes || "Yes") : (mapTranslations.no || "No")
}<br>
${
node.last_seen_us
@@ -633,10 +639,14 @@ function createChannelFilters(){
});
const routerOnly=document.getElementById("filter-routers-only");
const mqttOnly=document.getElementById("filter-mqtt-only");
routerOnly.checked = saved["routersOnly"] || false;
mqttOnly.checked = saved["mqttOnly"] || false;
routerOnly.addEventListener("change", saveFiltersToLocalStorage);
routerOnly.addEventListener("change", updateNodeVisibility);
mqttOnly.addEventListener("change", saveFiltersToLocalStorage);
mqttOnly.addEventListener("change", updateNodeVisibility);
updateNodeVisibility();
}
@@ -647,12 +657,14 @@ function saveFiltersToLocalStorage(){
state[ch] = document.getElementById(`filter-channel-${ch}`).checked;
});
state["routersOnly"] = document.getElementById("filter-routers-only").checked;
state["mqttOnly"] = document.getElementById("filter-mqtt-only").checked;
localStorage.setItem("mapFilters", JSON.stringify(state));
}
function updateNodeVisibility(){
const routerOnly = document.getElementById("filter-routers-only").checked;
const mqttOnly = document.getElementById("filter-mqtt-only").checked;
const activeChannels = [...channelSet].filter(ch =>
document.getElementById(`filter-channel-${ch}`).checked
);
@@ -662,6 +674,7 @@ function updateNodeVisibility(){
if(marker){
const visible =
(!routerOnly || n.isRouter) &&
(!mqttOnly || n.is_mqtt_gateway) &&
activeChannels.includes(n.channel);
visible ? map.addLayer(marker) : map.removeLayer(marker);
@@ -692,6 +705,7 @@ function shareCurrentView() {
function resetFiltersToDefaults(){
document.getElementById("filter-routers-only").checked = false;
document.getElementById("filter-mqtt-only").checked = false;
channelSet.forEach(ch => {
document.getElementById(`filter-channel-${ch}`).checked = true;
});

View File

@@ -366,6 +366,7 @@
<div><strong data-translate-lang="firmware">Firmware</strong><strong>: </strong> <span id="info-firmware"></span></div>
<div><strong data-translate-lang="role">Role</strong><strong>: </strong> <span id="info-role"></span></div>
<div><strong data-translate-lang="mqtt_gateway">MQTT Gateway</strong><strong>: </strong> <span id="info-mqtt-gateway"></span></div>
<div><strong data-translate-lang="channel">Channel</strong><strong>: </strong> <span id="info-channel"></span></div>
<div><strong data-translate-lang="latitude">Latitude</strong><strong>: </strong> <span id="info-lat"></span></div>
<div><strong data-translate-lang="longitude">Longitude</strong><strong>: </strong> <span id="info-lon"></span></div>
@@ -710,6 +711,8 @@ async function loadNodeInfo(){
document.getElementById("info-hw-model").textContent = node.hw_model ?? "—";
document.getElementById("info-firmware").textContent = node.firmware ?? "—";
document.getElementById("info-role").textContent = node.role ?? "—";
document.getElementById("info-mqtt-gateway").textContent =
node.is_mqtt_gateway ? (nodeTranslations.yes || "Yes") : (nodeTranslations.no || "No");
document.getElementById("info-channel").textContent = node.channel ?? "—";
document.getElementById("info-lat").textContent =

View File

@@ -266,13 +266,14 @@ select, .export-btn, .search-box, .clear-btn {
<th data-translate-lang="last_lat">Last Latitude <span class="sort-icon"></span></th>
<th data-translate-lang="last_long">Last Longitude <span class="sort-icon"></span></th>
<th data-translate-lang="channel">Channel <span class="sort-icon"></span></th>
<th data-translate-lang="mqtt_gateway">MQTT</th>
<th data-translate-lang="last_seen">Last Seen <span class="sort-icon"></span></th>
<th data-translate-lang="favorite"></th>
</tr>
</thead>
<tbody id="node-table-body">
<tr>
<td colspan="10" style="text-align:center; color:white;" data-translate-lang="loading_nodes">
<td colspan="11" style="text-align:center; color:white;" data-translate-lang="loading_nodes">
Loading nodes...
</td>
</tr>
@@ -448,7 +449,7 @@ document.addEventListener("DOMContentLoaded", async function() {
setStatus("");
} catch (err) {
tbody.innerHTML = `<tr>
<td colspan="10" style="text-align:center; color:red;">
<td colspan="11" style="text-align:center; color:red;">
${nodelistTranslations.error_loading_nodes || "Error loading nodes"}
</td></tr>`;
setStatus("");
@@ -583,7 +584,7 @@ document.addEventListener("DOMContentLoaded", async function() {
if (!nodes.length) {
if (shouldRenderTable) {
tbody.innerHTML = `<tr>
<td colspan="10" style="text-align:center; color:white;">
<td colspan="11" style="text-align:center; color:white;">
${nodelistTranslations.no_nodes_found || "No nodes found"}
</td>
</tr>`;
@@ -613,6 +614,7 @@ document.addEventListener("DOMContentLoaded", async function() {
<td>${node.last_lat ? (node.last_lat / 1e7).toFixed(7) : "N/A"}</td>
<td>${node.last_long ? (node.last_long / 1e7).toFixed(7) : "N/A"}</td>
<td>${node.channel || "N/A"}</td>
<td>${node.is_mqtt_gateway ? (nodelistTranslations.yes || "Yes") : (nodelistTranslations.no || "No")}</td>
<td>${timeAgoFromMs(node.last_seen_ms)}</td>
<td style="text-align:center;">
<span class="favorite-star ${fav ? "active" : ""}" data-node-id="${node.node_id}">

View File

@@ -117,6 +117,7 @@ async def api_nodes(request):
"last_lat": getattr(n, "last_lat", None),
"last_long": getattr(n, "last_long", None),
"channel": n.channel,
"is_mqtt_gateway": getattr(n, "is_mqtt_gateway", None),
# "last_update": n.last_update.isoformat(),
"first_seen_us": n.first_seen_us,
"last_seen_us": n.last_seen_us,

View File

@@ -239,6 +239,7 @@ async def load_database_from_mqtt(
# -------------------------
async def main():
check_optional_deps()
await mqtt_store.load_gateway_cache()
logger = logging.getLogger(__name__)
# Initialize database