/* ============================================================================ GC · INSTÄLLNINGAR — driftrattarna, hans kortform ---------------------------------------------------------------------------- RÄTTELSE 2026-08-16: den första versionen påstod att alla fyra rattar i hans InstallningarView saknade backend-fält. Fel — tre av fyra finns, under andra namn, och gamla designen har justerat dem sedan 30/7 (settings-mode.jsx): hans "Lägsta säljpris för sol" → sellsolar_min_price_ore (styr även den automatiska solladdningen av bilen — hans fjärde ratt behövs därför inte) hans "Undvik nätköp" → maxprofit_import_aversion_kr (kr; UI öre) hans "Handelsmarginal" → cycle_cost_ore/cycle_cost_kwh, vår "Batterislitage": HEMS handlar bara när vinsten överstiger slitaget per kWh hans "Prisgräns för solladdning" → finns inte som fält, och ska enligt backend 16/8 INTE byggas: säljgolvet ovan styr samma sak i dag. Produkt- beslut hos André. Dessutom har vi två rattar han inte har: "Fyll batteriet tidigt" och "Morgonbuffert". Spann bekräftade av backend 16/8: sellsolar 0–500 öre, maxprofit 0–0,5 kr (visas som 0–50 öre), cycle_cost 0–100 öre default 10. 0 = AV, null = flottans värde — två skilda saker. Prisgränsen för solladdning byggs INTE som egen ratt (André 16/8): säljgolvet ovan styr redan samma sak — under det priset säljs inte solen, den används. Två rattar för ett beslut vore två sanningar. REGLERNA ÄR SPEC_GRAVIO_USER_MODE_UI §3.3 och de är hårda: - null = OJUSTERAD, inte 0. En ojusterad ratt visar flottans värde som ghost och skickas ALDRIG i payloaden. - En ratt som VAR satt och återställs skickas som explicit null. - Master-gaten raderar inga värden när den slås av — backend ignorerar dem. - Batterislitaget är INTE gate-styrt: det gäller alltid. Sparvägen är gcSaveSettings (gc-core): hela dokumentet, settings_rev med, en omläsning vid 409. PATCH /api/settings tar bara ev_session. ========================================================================== */ /* Flottans kalibrerade värden — visas som ghost, skickas aldrig. Samma tal som settings-mode.jsx FLEET_DEFAULTS; ändras de där ska de ändras här. maxprofit visas i öre (backend bär kr). */ const GCI_FLEET = { traj_premie_ore_per_kwh_h: 3, sellsolar_min_price_ore: 30, maxprofit_import_aversion_kr: 25, soc_planning_buffer_pct: 2, }; const GCI_KNOBS = [ { field:'traj_premie_ore_per_kwh_h', label:'Fyll batteriet tidigt', min:0, max:15, step:1, unit:'', gated:true, desc:'Högre värde = batteriet laddas fullt tidigare på dagen som säkerhet mot prognosfel. Lägre = ren prisoptimering, som kan vänta till de billigaste timmarna.' }, { field:'sellsolar_min_price_ore', label:'Lägsta säljpris för sol', min:0, max:500, step:5, unit:'öre', gated:true, desc:'Under detta pris säljs solen aldrig — den går till huset och batteriet i stället. Ratten styr också den automatiska solladdningen av bilen: under gränsen får bilen ta det överskott batteriet inte hinner ta, över den säljs solen. Planerad laddning påverkas inte — har du satt mål och avresa laddas bilen oavsett pris. 0 stänger av automatiken.' }, { field:'maxprofit_import_aversion_kr', label:'Undvik nätköp', min:0, max:50, step:1, unit:'öre', factor:100, gated:true, desc:'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.' }, { field:'soc_planning_buffer_pct', label:'Morgonbuffert', min:0, max:20, step:1, unit:'%', gated:true, desc:'Extra reserv ovanpå din lägsta batterinivå som kvällens planering sparar till morgonen. Vid midnatt släpps spärren.' }, ]; const GCI_ALWAYS = { field:'cycle_cost_ore', label:'Batterislitage', min:0, max:100, step:1, unit:'öre', desc:'Slitage per omsatt kWh. Högre värde gör HEMS mer sparsam med laddcyklerna — den handlar bara när vinsten överstiger slitaget. Det är den ratt som i hans prototyp heter Handelsmarginal.' }; /* Läser backendens settings-dokument. null/saknad = ojusterad. */ function gciRead(s){ const d = s || {}; const num = (v) => (v === null || v === undefined || v === '') ? null : Number(v); const out = { traj_premie_ore_per_kwh_h: num(d.traj_premie_ore_per_kwh_h), sellsolar_min_price_ore: num(d.sellsolar_min_price_ore), maxprofit_import_aversion_kr: d.maxprofit_import_aversion_kr == null ? null : Math.round(Number(d.maxprofit_import_aversion_kr)*100), soc_planning_buffer_pct: num(d.soc_planning_buffer_pct), cycle_cost_ore: d.cycle_cost_kwh ?? d.cycle_cost_ore ?? 10, }; out.user_mode_enabled = (d.user_mode_enabled === true || d.user_mode_enabled === false) ? d.user_mode_enabled : GCI_KNOBS.some(k => out[k.field] != null); return out; } function GciKnob({ k, value, fleet, onChange, disabled }){ const set = value != null; const shown = set ? value : fleet; return (
{k.label} {shown}{k.unit ? ' ' + k.unit : ''} {!set && Gravios standard}
onChange(Number(e.target.value))} className="mt-1.5 w-full accent-sky-400"/>

{k.desc}

{set && ( )}
); } function GcInstallningar({ settings, onSaved }){ /* "Inte hämtat än" är INTE samma sak som "inga rattar satta". Utan den här vakten härleder gciRead(undefined) user_mode_enabled till false under den dryga sekund settings saknas — fliken visar då "Automatisk drift" fast backend säger true. Värre: rör användaren gaten i det fönstret sätts dirty, re-seedingen hoppas över permanent, och Spara postar user_mode_enabled:false. Exakt samma fälla som EV-kortet hade när det läste gammal config efter sparning (m0045). */ if (!settings) { return (
Hämtar inställningar…
); } return ; } function GcInstallningarBody({ settings, onSaved }){ const incoming = React.useMemo(() => gciRead(settings), [settings]); const sig = JSON.stringify(incoming); const [v, setV] = React.useState(incoming); const [dirty, setDirty] = React.useState(false); const [saving, setSaving] = React.useState(false); const [toast, setToast] = React.useState(''); React.useEffect(() => { if (!dirty) setV(incoming); }, [sig]); const set = (patch) => { setV({ ...v, ...patch }); setDirty(true); setToast(''); }; const save = async () => { setSaving(true); try { await window.gcSaveSettings((cur) => { const base = gciRead(cur); const payload = { ...cur, user_mode_enabled: !!v.user_mode_enabled }; /* §3.3: satt värde skickas, ojusterat UTELÄMNAS, och en ratt som var satt och nu är återställd skickas som explicit null. Baseline läses ur det FÄRSKA dokumentet — annars kan en omläsning efter 409 skicka null för en ratt som hann bli satt av någon annan. */ for (const k of GCI_KNOBS){ const val = v[k.field]; const wasSet = base[k.field] != null; if (val != null) payload[k.field] = k.factor ? Number(val)/k.factor : Number(val); else if (wasSet) payload[k.field] = null; else delete payload[k.field]; } payload.cycle_cost_kwh = Number(v.cycle_cost_ore); return payload; }); setDirty(false); setToast('ok'); if (onSaved) onSaved(); } catch(e){ setToast(e && /ändrades av någon annan/.test(e.message) ? 'conflict' : 'err'); } setSaving(false); }; const on = !!v.user_mode_enabled; return (
Driftinställningar

Värdena styr hur HEMS prioriterar mellan att lagra, sälja och köpa. Gravios standardvärden passar de flesta anläggningar — rör ratten bara om du vill styra åt ett särskilt håll.

{[{ id:false, t:'Automatisk drift', d:'Gravios kalibrerade värden' }, { id:true, t:'Egna inställningar', d:'du skruvar själv' }].map(o => ( ))}
{!on && (

HEMS kör flottans inställningar. Dina egna värden ligger kvar och används igen när du byter tillbaka.

)}
Egna värden {on ? 'aktiva' : 'inaktiva just nu'}
{GCI_KNOBS.map(k => ( set({ [k.field]:x })}/> ))}
Gäller alltid
set({ cycle_cost_ore: x == null ? 10 : x })}/>
{toast === 'ok' && Sparat — gäller från nästa planering} {toast === 'err' && Kunde inte spara — försök igen} {toast === 'conflict' && Ändrat på annat håll — läs in sidan igen} {dirty && !toast && Osparade ändringar}
Alla inställningar Batteri, sol, elavtal, effektavgift, laddboxar och bilprofiler.
); } Object.assign(window, { GcInstallningar, GcInstallningarBody, gciRead });