diff --git a/frontend/src/components/StatePanel.tsx b/frontend/src/components/StatePanel.tsx index 8e80af2..442529b 100644 --- a/frontend/src/components/StatePanel.tsx +++ b/frontend/src/components/StatePanel.tsx @@ -8,6 +8,37 @@ export type StatePanelProps = { nowIndex: number } +function deyeModeLabel(s: SlotData): string { + const m = s.deye_physical_mode + if (m === 'SELL') return 'SELL' + if (m === 'CHARGE') return 'CHARGE' + if (m === 'PASSIVE') return 'PASSIVE' + return '—' +} + +function deyeModeBadge(s: SlotData): { label: string; klass: string; title: string } { + const m = s.deye_physical_mode + if (m === 'SELL') { + return { + label: 'SELL', + klass: 'bg-orange-500/15 text-orange-200 ring-1 ring-orange-500/35', + title: 'SELL (selling first): reg142=0, reg178=32 (grid peak shaving off)', + } + } + if (m === 'CHARGE') { + return { + label: 'CHARGE', + klass: 'bg-sky-500/15 text-sky-200 ring-1 ring-sky-500/35', + title: 'CHARGE (grid charge): TOU grid_charge enabled v time pointech; reg178=48', + } + } + return { + label: 'PASSIVE', + klass: 'bg-slate-600/40 text-slate-200 ring-1 ring-slate-500/30', + title: 'PASSIVE (ZERO): reg142=deye_zero_export_mode; reg178=48', + } +} + /** Stav segmentu pro jeden track */ export type TrackSegment = { widthPct: number @@ -431,6 +462,21 @@ function StatePanelRaw({ slots, nowIndex }: StatePanelProps) {

Energetický tok

+

+ Deye režim (plán):{' '} + {(() => { + const b = deyeModeBadge(slots[nowIndex]!) + return ( + + {b.label} + + ) + })()} + {deyeModeLabel(slots[nowIndex]!)} +

diff --git a/frontend/src/hooks/useDashboardData.ts b/frontend/src/hooks/useDashboardData.ts index 4c3b73e..0b4b248 100644 --- a/frontend/src/hooks/useDashboardData.ts +++ b/frontend/src/hooks/useDashboardData.ts @@ -94,6 +94,7 @@ function emptySlot(iso: string): SlotData { battery_setpoint_w: null, grid_power_w: null, grid_setpoint_w: null, + deye_physical_mode: null, load_power_w: null, gen_port_power_w: null, pv_a_forecast_w: null, @@ -118,6 +119,7 @@ function emptySlot(iso: string): SlotData { function mergeInterval(s: SlotData, p: PlanningIntervalDto): void { s.battery_setpoint_w = p.battery_setpoint_w ?? s.battery_setpoint_w s.grid_setpoint_w = p.grid_setpoint_w ?? s.grid_setpoint_w + if (p.deye_physical_mode != null) s.deye_physical_mode = p.deye_physical_mode s.ev1_setpoint_w = p.ev1_setpoint_w ?? s.ev1_setpoint_w s.ev2_setpoint_w = p.ev2_setpoint_w ?? s.ev2_setpoint_w if (s.ev1_setpoint_w == null && s.ev2_setpoint_w == null && p.ev_charge_power_w != null) { diff --git a/frontend/src/pages/Planning.tsx b/frontend/src/pages/Planning.tsx index 4119c3d..c835957 100644 --- a/frontend/src/pages/Planning.tsx +++ b/frontend/src/pages/Planning.tsx @@ -187,6 +187,7 @@ function syntheticForecastOnlyInterval( battery_setpoint_w: null, battery_soc_target_pct: null, grid_setpoint_w: null, + deye_physical_mode: null, ev1_setpoint_w: null, ev2_setpoint_w: null, heat_pump_enabled: null, @@ -284,8 +285,6 @@ function axiosDetail(e: unknown): string { function deyeSetpointLabel(i: PlanningIntervalDto): string { const battery_w = i.battery_setpoint_w ?? 0 const grid_w = i.grid_setpoint_w ?? 0 - const is_exporting = battery_w < -500 || grid_w < -500 - const is_charging = battery_w > 500 const tgt = i.battery_soc_target_pct const targetSoc = tgt != null ? Math.min(95, Math.round(Number(tgt))) : 80 @@ -295,14 +294,49 @@ function deyeSetpointLabel(i: PlanningIntervalDto): string { return `${s}kW` } - if (is_exporting) { + const pm = (i.deye_physical_mode ?? '').toString().trim().toUpperCase() + if (pm === 'SELL') { const tpPowerW = Math.abs(battery_w) - return `⬇ ${fmtKw(tpPowerW)} | reg178 bit4–5=10 (grid PS off)` + return `SELL | ⬇ ${fmtKw(tpPowerW)} | reg142=0 reg178=32` } - if (is_charging) { - return `⬆ ${fmtKw(battery_w)} | grid=yes | SOC→${targetSoc}%` + if (pm === 'CHARGE') { + return `CHARGE | ⬆ ${fmtKw(Math.max(0, battery_w))} | grid=yes | SOC→${targetSoc}%` + } + + // PASSIVE (ZERO): doplň informaci o variantě 108/109 podle pravidel (bez wattových prahů). + if (grid_w < 0 && battery_w >= 0) return 'PASSIVE | FVE→síť (108=0)' + if (grid_w > 0 && battery_w <= 0) return 'PASSIVE | držet bat. (109=0)' + return 'PASSIVE | max/max' +} + +function deyeModeBadge(i: PlanningIntervalDto): { label: string; klass: string; title: string } { + const m = (i.deye_physical_mode ?? 'PASSIVE').toString().trim().toUpperCase() + const battery_w = i.battery_setpoint_w ?? 0 + const grid_w = i.grid_setpoint_w ?? 0 + + if (m === 'SELL') { + return { + label: 'SELL', + klass: 'bg-orange-500/15 text-orange-200 ring-1 ring-orange-500/35', + title: 'SELL (selling first): reg142=0, reg178=32 (grid peak shaving off)', + } + } + if (m === 'CHARGE') { + return { + label: 'CHARGE', + klass: 'bg-sky-500/15 text-sky-200 ring-1 ring-sky-500/35', + title: 'CHARGE (grid charge): TOU grid_charge enabled v time pointech; reg178=48', + } + } + + let variant = 'max/max' + if (grid_w < 0 && battery_w >= 0) variant = 'FVE→síť (108=0)' + else if (grid_w > 0 && battery_w <= 0) variant = 'držet bat. (109=0)' + return { + label: 'PASSIVE', + klass: 'bg-slate-600/40 text-slate-200 ring-1 ring-slate-500/30', + title: `PASSIVE (ZERO): ${variant}; reg142=deye_zero_export_mode; reg178=48`, } - return '~ 2kW | hold' } function tableRowClass( @@ -1114,7 +1148,22 @@ export default function Planning() { {i.battery_setpoint_w ?? '—'} - {deyeSetpointLabel(i)} +
+ {(() => { + const b = deyeModeBadge(i) + return ( + + {b.label} + + ) + })()} + + {deyeSetpointLabel(i)} + +
{i.battery_soc_target_pct != null diff --git a/frontend/src/types/dashboard.ts b/frontend/src/types/dashboard.ts index 3733b26..1f43928 100644 --- a/frontend/src/types/dashboard.ts +++ b/frontend/src/types/dashboard.ts @@ -8,6 +8,8 @@ export type SlotData = { grid_power_w: number | null /** Plánovaný výkon sítě (W) – budoucí sloty. */ grid_setpoint_w: number | null + /** Fyzický režim Deye pro slot (PASSIVE/SELL/CHARGE) z plánu. */ + deye_physical_mode: 'PASSIVE' | 'SELL' | 'CHARGE' | null load_power_w: number | null gen_port_power_w: number | null pv_a_forecast_w: number | null diff --git a/frontend/src/types/plan.ts b/frontend/src/types/plan.ts index 2a652bc..3d17b6a 100644 --- a/frontend/src/types/plan.ts +++ b/frontend/src/types/plan.ts @@ -15,6 +15,8 @@ export type PlanningIntervalDto = { battery_setpoint_w: number | null battery_soc_target_pct: number | null grid_setpoint_w: number | null + /** Explicitní fyzický režim Deye pro slot (PASSIVE/SELL/CHARGE). */ + deye_physical_mode?: 'PASSIVE' | 'SELL' | 'CHARGE' | null ev1_setpoint_w: number | null ev2_setpoint_w: number | null ev_charge_power_w?: number | null