/* settings-mode.jsx — Driftläge: "Automatisk drift" (fka Gravio-Mode) vs "Egna inställningar" (fka User-mode). SPEC_GRAVIO_USER_MODE_UI 2026-07-30. Namnförslag till André (arbetsnamnen Gravio-Mode/User-mode är inte husägar- språk): AUTOMATISK DRIFT / EGNA INSTÄLLNINGAR. Byt strängarna i MODE_OPTS. self_sufficiency-läget är RIVET — operating_mode läses/skrivs inte längre någonstans i UI:t (se settings.jsx toBackend). HÅRDA KRAV som koden bär (§3.3): - null = "ojusterad" ≠ 0 = "av". Ojusterade rattar visar flottans värde som GHOST (placeholder), aldrig som satt värde, och skickas ALDRIG i payloaden. - Master-gaten (user_mode_enabled) raderar inga rattvärden när den slås av — backend ignorerar dem, UI:t minns dem. - Ratt 5 (batterislitage) och EV-prioriteringen mode-gatas INTE: de gäller alltid och ligger utanför gaten i renderingen. */ // Flottans kalibrerade defaults — visas som ghost, skickas aldrig av UI:t. // Ändrar André flottans värden ska bara dessa siffror uppdateras. const FLEET_DEFAULTS = { traj_premie_ore_per_kwh_h: 3, sellsolar_min_price_ore: 30, maxprofit_import_aversion_kr: 0.25, soc_planning_buffer_pct: 2, cycle_cost_ore: 10, }; // soc_planning_buffer_pct: LÅST UPP 2026-07-30 (André: "morgonbufferten ska // vara klar att använda"). Ratten är fullt justerbar och POSTas som de andra. const SOC_BUFFER_READY = true; // Rattarna som får POSTas som null vid "återställ" (mode-gatade + fria). // cycle_cost_ore ligger INTE här: det är ett vanligt alltid-fält (cycle_cost_kwh). const MODE_KNOB_FIELDS = [ 'traj_premie_ore_per_kwh_h', 'sellsolar_min_price_ore', 'maxprofit_import_aversion_kr', ...(SOC_BUFFER_READY ? ['soc_planning_buffer_pct'] : []), ]; const MODE_OPTS = [ { value:false, label:'Automatisk drift', help:'HEMS kör Gravios kalibrerade inställningar — samma som hela flottan. Du behöver inte ställa in något, och förbättrar vi inställningarna följer ditt hus med automatiskt.' }, { value:true, label:'Egna inställningar', help:'Samma motor, men du får själv skruva på ett antal värden — hur tidigt batteriet fylls, när solen säljs och hur ogärna huset köper el. Allt står på Gravios standard tills du ändrar det.' }, ]; const KNOBS = [ { // Enheten "öre" är BORTTAGEN (utvecklaren 2026-07-30): parametern är en // viktning, inte ett pris — att visa öre fick den att läsas som en kostnad. // Skalan är enhetslös 0–15. Fältnamnet behåller sitt backend-suffix. field:'traj_premie_ore_per_kwh_h', label:'Fyll batteriet tidigt', min:0, max:15, step:1, unit:'', dec:0, scale:true, help:'Högre värde = batteriet laddas fullt tidigare på dagen som säkerhet mot prognosfel. Lägre = ren prisoptimering, som kan vänta med laddningen till de billigaste timmarna.', zero:'Av — ren prisoptimering, ingen premie för tidig laddning.', }, { field:'sellsolar_min_price_ore', label:'Lägsta säljpris för sol', min:0, max:100, step:1, unit:'öre', dec:0, help:'Under detta pris säljs solen aldrig — den lagras i batteriet i stället. Är batteriet fullt flödar överskottet ändå ut, så ingen sol slösas.', zero:'Av — solen säljs vid alla priser.', }, { // Visas i ÖRE, lagras i KRONOR. Blandade enheter i samma panel var // förvirrande (André 2026-07-30) — allt som är pengar är nu öre. // factor:100 ⇒ UI-värde = backend-värde × 100. Backend-fältet är oförändrat. field:'maxprofit_import_aversion_kr', label:'Undvik nätköp', min:0, max:50, step:1, unit:'öre', dec:0, factor:100, help:'Högre värde = batteriet täcker huset i stället för att köpa el, även när en liten handelsvinst går förlorad. Vid höga värden avstår systemet också billiga nattladdningar med liten marginal — det är avsiktligt.', zero:'Av — ren arbitrage, HEMS köper när det är lönsamt.', }, { // Hette "Kvällsbuffert" — döpt till Morgonbuffert (utvecklaren 2026-07-30): // bufferten SPARAS på kvällen men finns TILL för morgonen, och namnet ska // beskriva vad den skyddar. Mekaniken enligt utvecklaren: kvällens planering // lägger bufferten OVANPÅ min-SoC (min 3 % + 2 % ⇒ planerar mot 5 %); kl 00:00 // släpps spärren och batteriet får gå ned till min-SoC igen; bufferten ska // vara återfylld till 23:45 så nästa dygns planering har sina 2 % extra. field:'soc_planning_buffer_pct', label:'Morgonbuffert', min:0, max:20, step:1, unit:'%', dec:0, help:'Extra reserv OVANPÅ din lägsta batterinivå, som kvällens planering sparar till morgonen. Har du lägsta nivå 3 % planerar HEMS mot 5 %. Vid midnatt släpps spärren och batteriet får användas ned till 3 % igen, och bufferten är återfylld till kl 23:45 inför nästa dygn. Syftet är att natten inte ska behöva köpa el om förbrukningen blir högre än planerat.', zero:'Av — ingen extra reserv över natten, planeringen går ned till din lägsta batterinivå.', pending: !SOC_BUFFER_READY, // false sedan 2026-07-30 }, ]; // ── En ratt: slider + siffra, ghost när ojusterad ───────────────────────── function Knob({ k, value, onChange, onReset, always }){ // f = UI-enhet per backend-enhet (100 för öre-visning av ett kr-fält). // Allt i den här komponenten räknas i UI-enhet; bara onChange går tillbaka. const f = k.factor || 1; const toUi = (v) => Number(v) * f; const toApi = (v) => f === 1 ? v : Math.round(Number(v) * 1e6 / f) / 1e6; const def = toUi(FLEET_DEFAULTS[k.field]); const unset = value === null || value === undefined || value === ''; const shown = unset ? def : toUi(value); const fmt = (n) => Number(n).toFixed(k.dec).replace('.', ',').replace(/,0+$/, ''); // Enhetslösa rattar (t.ex. "Fyll batteriet tidigt") får ingen enhet alls — // ett hängande mellanslag före punkten ser ut som en bugg. const val = (n) => fmt(n) + (k.unit ? ' ' + k.unit : ''); const commit = (raw) => { if (raw === '' ) { onReset(); return; } let n = parseFloat(String(raw).replace(',', '.')); if (isNaN(n)) return; n = Math.min(k.max, Math.max(k.min, n)); onChange(toApi(n)); }; return (
{k.label} {k.pending ? Snart tillgänglig : always ? Gäller alltid : unset ? Gravios standard : }
{k.help}
commit(e.target.value)}/>
commit(e.target.value)}/> {k.unit ? {k.unit} : null}
{k.pending ? Kör på Gravios standard {val(def)}. Ratten går att ställa in när HEMS uppdaterats. : unset ? <>Står på Gravios standard {val(def)}{k.scale ? <> på skalan {k.min}–{k.max} : null}. Rör ratten för att sätta ett eget värde. : Number(shown) === 0 ? {k.zero} : <>Ditt värde: {val(toUi(value))} (Gravios standard: {val(def)}).}
); } // ── Bunker-summering: mjuk vakt, inga spärrar (§3.4) ────────────────────── function bunkerNote(s){ const av = s.maxprofit_import_aversion_kr, fl = s.sellsolar_min_price_ore, tr = s.traj_premie_ore_per_kwh_h; const n = (v, d) => (v === null || v === undefined || v === '') ? d : Number(v); // NB: kr här, inte öre — bunkerNote läser settings-objektet (backend-enhet). const a = n(av, FLEET_DEFAULTS.maxprofit_import_aversion_kr) / 0.5; const f = n(fl, FLEET_DEFAULTS.sellsolar_min_price_ore) / 100; const t = n(tr, FLEET_DEFAULTS.traj_premie_ore_per_kwh_h) / 10; if (a < 0.7 || (f < 0.6 && t < 0.7)) return null; // Schablon: A/B-mätningen 30/7 gav ~2,5 kr/dag vid maximal import-aversion. const kr = Math.max(1, Math.round(2.5 * ((a + Math.max(f, t)) / 2) * 10) / 10); return String(kr).replace('.', ','); } // ── Kortet ──────────────────────────────────────────────────────────────── function RunModeCard({ s, u }){ const on = !!s.user_mode_enabled; const bunker = on ? bunkerNote(s) : null; const knob = (field) => KNOBS.find(k => k.field === field); const row = (field, extra) => ( u(field, v)} onReset={()=>u(field, null)} {...extra}/> ); return (
{MODE_OPTS.map(o => { const sel = on === o.value; return ( ); })}
{on && (
Dina rattar Allt du inte rör körs på Gravios standard.
{row('traj_premie_ore_per_kwh_h')} {row('sellsolar_min_price_ore')} {row('maxprofit_import_aversion_kr')} {row('soc_planning_buffer_pct')} {bunker && (
Din inställning liknar maximal självförsörjning. Förväntad merkostnad ca {bunker} kr/dag jämfört med automatisk drift. Inget hindrar dig — det är en avvägning, inte ett fel.
)}
)} {!on && (
Byter du till egna inställningar ligger dina tidigare värden kvar — och går du tillbaka hit körs Gravios standard igen.
)} {/* Utanför gaten: gäller i BÅDA lägena (§3.2 särregler) */}
Gäller alltid Egenskaper hos din anläggning — oberoende av driftläge.
u('cycle_cost_ore', v)} onReset={()=>u('cycle_cost_ore', FLEET_DEFAULTS.cycle_cost_ore)} always/> {/* "Pausa hembatteriet när elbilen laddar" BORTTAGEN här (André 31/8): valet bor nu i Bil-fliken under "Batteriet när bilen laddar" (två knappar, ev_pause_battery + ev_house_only) — två ingångar till samma bool hade låtit dem säga olika saker. */}
); } Object.assign(window, { RunModeCard, MODE_KNOB_FIELDS, FLEET_DEFAULTS, SOC_BUFFER_READY });