- R__097: vw_latest_pool_pump + vw_pool_pump_day_energy (denní kWh z delty čítače, minuty běhu) + ems_anon granty - PoolCard na Dashboardu: stav/W/dnešní kWh+hodiny/7denní mini sloupce - _notify_ev_arrival_plan: po příjezdu EV Discord souhrn (SoC auta → cíl, deadline, nabíjecí okna shlukovaná ze slotů aktivního plánu, ø cena) - docs/discord-ev-interaction.md: fáze B (bot s tlačítky přes gateway — žádný veřejný endpoint; čeká na DISCORD_BOT_TOKEN od uživatele) - docs: pool-shelly + ev-charging aktualizovány (pravidlo docs 1:1) První commit na dev větvi (nová kadence: deploy až s milníkovým merge). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
118 lines
3.7 KiB
TypeScript
118 lines
3.7 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { getJson } from '../api/postgrest'
|
|
|
|
type LatestRow = {
|
|
site_id: number
|
|
pump_id: number
|
|
pump_code: string
|
|
rated_power_w: number
|
|
schedulable: boolean
|
|
measured_at: string | null
|
|
is_on: boolean | null
|
|
power_w: number | null
|
|
energy_wh_total: number | null
|
|
}
|
|
|
|
type DayRow = { day: string; kwh: number; on_minutes: number }
|
|
|
|
const POLL_MS = 60_000
|
|
|
|
/** Karta bazénového čerpadla (Shelly): stav, příkon, dnešní kWh + týdenní mini přehled. */
|
|
export function PoolCard({ siteId }: { siteId: number }) {
|
|
const [latest, setLatest] = useState<LatestRow | null>(null)
|
|
const [days, setDays] = useState<DayRow[]>([])
|
|
|
|
useEffect(() => {
|
|
let alive = true
|
|
const load = async () => {
|
|
try {
|
|
const [l, d] = await Promise.all([
|
|
getJson<LatestRow[]>('/vw_latest_pool_pump', { site_id: `eq.${siteId}` }),
|
|
getJson<DayRow[]>('/vw_pool_pump_day_energy', {
|
|
site_id: `eq.${siteId}`,
|
|
order: 'day.desc',
|
|
limit: '7',
|
|
}),
|
|
])
|
|
if (!alive) return
|
|
setLatest(l[0] ?? null)
|
|
setDays(d)
|
|
} catch {
|
|
/* karta je doplňková — chybu nechováme */
|
|
}
|
|
}
|
|
void load()
|
|
const t = setInterval(load, POLL_MS)
|
|
return () => {
|
|
alive = false
|
|
clearInterval(t)
|
|
}
|
|
}, [siteId])
|
|
|
|
if (!latest) return null
|
|
|
|
const stale =
|
|
latest.measured_at != null &&
|
|
Date.now() - new Date(latest.measured_at).getTime() > 5 * 60_000
|
|
const running = latest.is_on === true
|
|
const today = days[0]
|
|
|
|
return (
|
|
<div className="rounded-xl border border-slate-800 bg-slate-900/60 p-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-lg">🏊</span>
|
|
<span className="text-sm font-medium text-slate-200">Bazénové čerpadlo</span>
|
|
<span
|
|
className={`inline-block h-2.5 w-2.5 rounded-full ${
|
|
stale ? 'bg-slate-600' : running ? 'bg-emerald-400' : 'bg-slate-500'
|
|
}`}
|
|
title={stale ? 'telemetrie >5 min stará' : running ? 'běží' : 'stojí'}
|
|
/>
|
|
</div>
|
|
<span className="text-xs text-slate-400">
|
|
{latest.schedulable ? 'řízeno plánem' : 'jen měření'}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="mt-3 grid grid-cols-3 gap-2 text-center">
|
|
<div>
|
|
<div className="text-lg font-semibold text-slate-100">
|
|
{stale ? '—' : running ? `${latest.power_w ?? 0} W` : '0 W'}
|
|
</div>
|
|
<div className="text-[11px] text-slate-400">příkon</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-lg font-semibold text-slate-100">
|
|
{today ? `${today.kwh.toFixed(2)}` : '—'}
|
|
</div>
|
|
<div className="text-[11px] text-slate-400">kWh dnes</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-lg font-semibold text-slate-100">
|
|
{today ? `${Math.round(today.on_minutes / 60 * 10) / 10} h` : '—'}
|
|
</div>
|
|
<div className="text-[11px] text-slate-400">běh dnes</div>
|
|
</div>
|
|
</div>
|
|
|
|
{days.length > 1 && (
|
|
<div className="mt-3 flex items-end gap-1" title="kWh po dnech (7 dní)">
|
|
{[...days].reverse().map((d) => {
|
|
const max = Math.max(...days.map((x) => x.kwh), 0.01)
|
|
return (
|
|
<div key={d.day} className="flex-1">
|
|
<div
|
|
className="rounded-sm bg-sky-700/70"
|
|
style={{ height: `${Math.max(3, (d.kwh / max) * 36)}px` }}
|
|
title={`${d.day}: ${d.kwh.toFixed(2)} kWh`}
|
|
/>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|