mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-03-04 23:27:46 +01:00
Added the ability to track gateways and present them in varous pages
This commit is contained in:
27
alembic/versions/23dad03d2e42_add_is_mqtt_gateway_to_node.py
Normal file
27
alembic/versions/23dad03d2e42_add_is_mqtt_gateway_to_node.py
Normal 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")
|
||||
@@ -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",
|
||||
@@ -98,6 +101,7 @@
|
||||
|
||||
"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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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}">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user