import { useEffect, useState, useCallback } from 'react'; import { RadioTower, Cable, Wifi, Zap, Plus, Router, Waypoints } from 'lucide-react'; import { apiGet, apiPost } from '@/api/client'; import { cn } from '@/lib/utils'; import { LoraMeshViz } from './LoraMeshViz'; interface LoraDevice { node_id: string; name?: string | null; project?: string | null; object?: string | null; transport: 'lora' | 'lan'; status: 'pending' | 'active' | 'disabled'; caps?: { pzem?: boolean; relays?: number; ctl?: boolean }; relay_names?: string[]; fw?: string; ip?: string; gw?: string; last_seen?: string; last_rssi?: number; online?: boolean; v?: number; a?: number; w?: number; kwh?: number; pf?: number; hz?: number; relays?: number[]; } interface Gateway { gw_id: string; name: string; ip: string; online: boolean; nodes: number; avg_rssi: number } const lastSeen = (iso?: string) => { if (!iso) return '—'; const s = Math.round((Date.now() - new Date(iso).getTime()) / 1000); return s < 60 ? `${s} с назад` : s < 3600 ? `${Math.round(s / 60)} мин назад` : `${Math.round(s / 3600)} ч назад`; }; export function LoraModule() { const [devices, setDevices] = useState([]); const [gws, setGws] = useState([]); const [filter, setFilter] = useState<'all' | 'egorin' | 'orbit'>('all'); const load = useCallback(async () => { try { const [d, g] = await Promise.all([apiGet('/lora/devices'), apiGet('/lora/gateways')]); setDevices(d); setGws(g); } catch { /* ignore */ } }, []); useEffect(() => { load(); const t = setInterval(load, 3000); return () => clearInterval(t); }, [load]); const pending = devices.filter((d) => d.status === 'pending'); const active = devices.filter((d) => d.status === 'active' && (filter === 'all' || d.project === filter)); const approve = async (id: string) => { await apiPost(`/lora/devices/${id}/approve`, { name: id, project: 'egorin' }); load(); }; const toggleRelay = async (id: string, n: number, cur: number) => { await apiPost(`/lora/devices/${id}/cmd`, { cmd: 'relay', n, s: cur ? 0 : 1 }); load(); }; const sumKwh = active.reduce((s, d) => s + (d.kwh || 0), 0); const sumW = active.reduce((s, d) => s + (d.w || 0), 0); return (
Энерго LoRa/LAN
датчики PZEM → шлюз-концентратор → сервер · MQTT-топики nodes/<id>
{active.length} узлов Σ {(sumW / 1000).toFixed(1)} кВт Σ {sumKwh.toFixed(1)} кВт·ч
{/* Шлюзы */}
{gws.map((g) => (
{g.gw_id} {g.ip} · {g.nodes} узлов · RSSI ~{g.avg_rssi} дБм
))}
{/* Визуализатор ретрансляции/маршрутизации LoRa-хопов */}
Маршрутизация LoRa · хопы и ретрансляция
d.transport === 'lora' && d.status !== 'disabled').map((d) => ({ id: d.node_id, label: d.name || d.node_id, status: d.status, rssi: d.last_rssi, transport: d.transport }))} />
{/* Обнаружены новые устройства (pending) */} {pending.length > 0 && (
Обнаружены новые устройства ({pending.length})
{pending.map((d) => (
{d.node_id} реле: {d.caps?.relays ?? 0} · {lastSeen(d.last_seen)}
))}
)} {/* Фильтр-чипы */}
{(['all', 'egorin', 'orbit'] as const).map((f) => ( ))}
{/* Карточки узлов */}
{active.map((d) => (
{d.name || d.node_id}
{d.object || '—'} · {d.node_id}
{[['V', d.v, 'В'], ['A', d.a, 'А'], ['W', d.w, 'Вт'], ['kWh', d.kwh, 'кВт·ч']].map(([k, val, u]) => (
{val != null ? Number(val).toFixed(k === 'A' ? 2 : k === 'kWh' ? 1 : 0) : '—'}
{u as string}
))}
{(d.relays?.length ?? 0) > 0 && (
{d.relays!.map((r, n) => ( ))}
)}
{lastSeen(d.last_seen)}{d.transport === 'lora' && d.last_rssi != null ? ` · RSSI ${d.last_rssi} дБм` : ''}{d.ip ? ` · ${d.ip}` : ''}
))}
{active.length === 0 &&
Нет активных узлов в этом фильтре.
}
); } function Badge({ d }: { d: LoraDevice }) { const lora = d.transport === 'lora'; return ( {lora ? : } {lora ? 'LoRa' : 'LAN'} ); }