/* 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, soc_buffer_release_ore: 30, 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'] : []), /* B1 (backend 26/9): släpp av morgonbufferten på kvällen när kvällen är minst så här många öre dyrare än morgonen. */ 'soc_buffer_release_ore', ]; 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', // Taket höjt 50 → 100 öre (André 21/9). UI:t är öre, backend-fältet är kr // (factor 100), så 100 öre = 1,00 kr i payloaden. Backends båda tak (spar- // validering och planering) är höjda till 1,00 kr i samma deploy. min:0, max:100, step:1, unit:'öre', dec:0, factor:100, // U1: hjälptexten sa att nattladdningar uteblir "avsiktligt" utan att säga // vad det kostar. Omspelningen av tio staginghems planeringsindata 21/9 gav // dyrare plan och mindre försäljning ju högre ratten stod (+33 kr på tio // hem vid 100 öre mot 50) — den som drar till 100 ska inte tro att // självförsörjningen är gratis. help:'Högre värde = batteriet täcker huset i stället för att köpa el. Det kostar: HEMS säljer mindre och avstår billiga nattladdningar, så vinsten blir lägre ju högre du går.', /* U2: egen rad över 50 öre. Området 51–100 har aldrig mätts i drift — A/B-mätningen 30/7 gick till 50 — så raden säger riktningen och att nivån är outvärderad, utan kronbelopp (U3: inget extrapoleras hit). */ note: (ui) => ui > 50 ? 'Över 50 öre köper huset nästan ingen el alls, men försäljningen minskar tydligt. Nivån är inte utvärderad i drift ännu.' : null, 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. Är kvällen tydligt dyrare än morgonen kan bufferten släppas redan på kvällen, se Släpp morgonbufferten vid.', 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 }, { /* B1–B3 (TILL_DESIGN_morgonbuffert_slapp, backend 26/9): heltal öre 0–500, standard 30, gatat av Egna inställningar som Morgonbuffert. */ field:'soc_buffer_release_ore', label:'Släpp morgonbufferten vid', min:0, max:500, step:1, unit:'öre', dec:0, help:'Så mycket dyrare kvällen (kl 18–24) måste vara än morgonen därpå (kl 00–09, eller tills solen täcker) för att HEMS ska få använda morgonbufferten redan på kvällen. Avgörs när morgondagens priser kommit, runt kl 13. Nås inte skillnaden hålls bufferten till midnatt som vanligt. Standard 30 öre. Högre värde = bufferten hålls oftare, 0 = släpps så fort kvällen är dyrare.', zero:'Släpps så fort kvällen är dyrare än morgonen.', }, ]; // ── 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}
{/* Nivånot under foten (U2). Samma dämpade gula ton som kortets övriga noter — valet är tillåtet, så ingen röd varning. Göms när ratten är ojusterad, vilket också täcker "Återställ": då är shown = standard. */} {!k.pending && !unset && k.note && k.note(Number(shown)) && (
{k.note(Number(shown))}
)}
{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). /* Schablonen nedan vilar på A/B-mätningen 30/7, där "maximal import-aversion" var takets dåvarande 0,50 kr. Med taket höjt till 1,00 kr (21/9) klamps andelen på 1: att låta den gå till 2 hade dubblat kronbeloppet med stöd i en mätning som aldrig gjordes där. */ const a = Math.min(1, 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')} {row('soc_buffer_release_ore')} {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 });