import axios from 'axios' import { RefreshCw } from 'lucide-react' import { memo, useCallback, useEffect, useState } from 'react' import { getCommandJournal, getDeyeRegisters, type DeyeRegistersLive, type ModbusJournalCommandDto } from '../api/backend' const BATT_VOLTAGE_V = 51.2 const POLL_REGISTERS_MS = 30_000 const POLL_JOURNAL_MS = 60_000 const TZ = 'Europe/Prague' function fmtTime(iso: string): string { return new Date(iso).toLocaleString('cs-CZ', { timeZone: TZ, day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', }) } function ampsToKw(a: number | null | undefined): string { if (a == null || Number.isNaN(a)) return '—' return `${((a * BATT_VOLTAGE_V) / 1000).toFixed(2)} kW` } function fmtW(w: number | null | undefined): string { if (w == null || Number.isNaN(w)) return '—' return `${w} W` } function journalSignature(cmds: ModbusJournalCommandDto[]): string { return cmds .map( (c) => `${c.id}:${c.status}:${c.attempt_count}:${c.value_written ?? ''}:${c.value_verified ?? ''}`, ) .join('|') } function statusBadgeClass(status: string): string { const u = status.toLowerCase() if (u === 'verified') return 'bg-emerald-600/25 text-emerald-200 ring-1 ring-emerald-500/40' if (u === 'written') return 'bg-sky-600/25 text-sky-200 ring-1 ring-sky-500/40' if (u === 'pending' || u === 'retrying') return 'bg-slate-600/30 text-slate-300 ring-1 ring-slate-500/35' if (u === 'failed' || u === 'mismatch') return u === 'mismatch' ? 'bg-red-600/30 text-red-100 font-bold ring-1 ring-red-500/50' : 'bg-red-600/25 text-red-200 ring-1 ring-red-500/40' return 'bg-slate-600/30 text-slate-300 ring-1 ring-slate-500/35' } type LiveSectionProps = { live: DeyeRegistersLive | null liveLoading: boolean onRefresh: () => void } const LiveRegistersSection = memo( function LiveRegistersSection({ live, liveLoading, onRefresh }: LiveSectionProps) { return (

Živé registry

{live?.read_at ? (

Načteno: {fmtTime(live.read_at)}

) : null}
) }, (a, b) => a.liveLoading === b.liveLoading && a.live?.read_at === b.live?.read_at && a.live?.reg108_charge_a === b.live?.reg108_charge_a && a.live?.reg109_discharge_a === b.live?.reg109_discharge_a && a.live?.reg141_energy_mode === b.live?.reg141_energy_mode && a.live?.reg142_limit_control === b.live?.reg142_limit_control && a.live?.reg143_export_limit_w === b.live?.reg143_export_limit_w && a.live?.reg178_peak_shaving_switch === b.live?.reg178_peak_shaving_switch && a.live?.reg178_control_board_special_1 === b.live?.reg178_control_board_special_1 && a.live?.reg178_mi_export_cutoff_bits === b.live?.reg178_mi_export_cutoff_bits && a.live?.reg178_mi_export_cutoff_is_on === b.live?.reg178_mi_export_cutoff_is_on && a.live?.reg191_peak_shaving_w === b.live?.reg191_peak_shaving_w, ) type MetricProps = { label: string reg: number unitA?: number | null kwHint?: boolean valueText?: string sub?: string } function Metric({ label, reg, unitA, kwHint, valueText, sub }: MetricProps) { const main = valueText ?? (unitA != null && !Number.isNaN(unitA) ? `${unitA} A` : '—') const extra = kwHint ? ampsToKw(unitA ?? null) : null return (

{label}

reg {reg}: {main} {extra && extra !== '—' ? · {extra} : null}

{sub ?

{sub}

: null}
) } type JournalSectionProps = { commands: ModbusJournalCommandDto[] } const JournalSection = memo( function JournalSection({ commands }: JournalSectionProps) { return (

Posledních 50 zápisů

{commands.length === 0 ? ( ) : ( commands.map((c) => ( )) )}
Čas Reg Popis Hodnota Pokus Status
Žádné záznamy v journalu.
{fmtTime(c.created_at)} {c.register} {c.register_name ?? '—'} {c.value_to_write} {c.value_verified != null ? ( → {c.value_verified} ) : null} {c.attempt_count} {c.status}
) }, (a, b) => journalSignature(a.commands) === journalSignature(b.commands), ) function ControlPanelImpl({ siteId }: { siteId: number }) { const [live, setLive] = useState(null) const [liveError, setLiveError] = useState(null) const [liveLoading, setLiveLoading] = useState(false) const [commands, setCommands] = useState([]) const [journalError, setJournalError] = useState(null) const fetchRegisters = useCallback(async () => { setLiveLoading(true) setLiveError(null) try { const data = await getDeyeRegisters(siteId) setLive(data) } catch (e: unknown) { let msg = 'Chyba čtení registrů' if (axios.isAxiosError(e)) { const d = e.response?.data as { detail?: string } | undefined if (typeof d?.detail === 'string') msg = d.detail } else if (e instanceof Error) { msg = e.message } setLiveError(msg) setLive(null) } finally { setLiveLoading(false) } }, [siteId]) const fetchJournal = useCallback(async () => { setJournalError(null) try { const res = await getCommandJournal(siteId, 50) setCommands(res.commands) } catch (e: unknown) { const msg = e instanceof Error ? e.message : 'Chyba načtení journalu' setJournalError(msg) setCommands([]) } }, [siteId]) useEffect(() => { void fetchRegisters() }, [fetchRegisters]) useEffect(() => { void fetchJournal() }, [fetchJournal]) useEffect(() => { const t = window.setInterval(() => void fetchRegisters(), POLL_REGISTERS_MS) return () => window.clearInterval(t) }, [fetchRegisters]) useEffect(() => { const t = window.setInterval(() => void fetchJournal(), POLL_JOURNAL_MS) return () => window.clearInterval(t) }, [fetchJournal]) const apiError = liveError ?? journalError return (
{apiError ? (

Chyba API řízení / Modbus

{liveError ? (

GET …/control/registers: {liveError}

) : null} {journalError ? (

Journal: {journalError}

) : null}
) : null}
) } export const ControlPanel = memo(ControlPanelImpl)