<script lang="ts"> import { browser } from '$app/environment'; import { onMount } from 'svelte'; import { base64decode } from '../../../base64'; import { base } from '$app/paths' const profileModules = import.meta.glob("../../../../static/faisan/profiles/*.csv"); export let puissance: number export let prms: string = "" export let active: string = "" let initialised = false $: { console.log("active", active) initialised && updatePlot(puissance) } let Plotly: any let srv = import.meta.env.MODE == 'production' ? 'coturnix.fr' : 'd.coturnix.fr' onMount(async () => { console.log("browser", browser) if(browser) { //@ts-ignore Plotly = await import('plotly.js-dist') updatePlot(puissance) initialised = true } }) let prt: string[] = [ ] let pr: {type:string, conso: number}[] = [ ] let plotStyle = "" let profs: Map<string, { coefs: number[], total: number }> = new Map() let darkMode = false; if(browser) { darkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => { darkMode = !!event.matches redraw() }); } let re = /.*\/([^\/]*)\.csv/ for (const modulePath in profileModules) { console.log("module", modulePath) let matches = modulePath.match(re) console.log(matches) if(matches) { prt.push(matches[1]) } } function addProfil(_ev: Event) { pr.push({ type: prt[0], conso: 0 }) pr = pr } function delProfil(i: number) { pr.splice(i, 1) pr = pr } async function profile(name: string): Promise<{ coefs: number[], total: number }> { let resp = await fetch( `${base}/profiles/${name}.csv`, ) let p = await resp.text() let annee = [] let total = 0 for(let line of p.split('\n')) { if(!line) { continue } const col = line.split(';') // const week = parseInt(col[0]) // const dow = parseInt(col[1]) const cs = parseFloat(col[2]) const cj = parseFloat(col[3]) for(let i = 0; i < 48; i++) { let ch = parseFloat(col[5+i]) const c = cs * cj * ch annee.push(c) total += c } } return { coefs: annee, total } } let annee: Record<string, string[]> = {} async function refresh(event: SubmitEvent) { event.preventDefault() if(prms) { let f = new FormData() f.append("prms", prms); let resp = await fetch( `https://${srv}/api/faisan`, { method: "POST", body: f }) if(resp.ok) { annee = await resp.json() } } if(prms || pr.length > 0) { await updatePlot(puissance) } } let auto = 0 let totalProd = 0 let totalConso = 0 let soleil = { samples: <[Date, number][]>[], x: <string[]>[], y: <number[]>[], type: 'lines', line: { color: "#ffbf00", width: 1, }, name: "Production solaire", bucket: 1, } let somme = { samples: <[Date, number][]>[], x: <string[]>[], y: <number[]>[], type: 'lines', line: { color: "#b81111", width: 1, }, name: "Consommation totale", bucket: 1, } async function updatePlot(puissance: number) { if(!browser) { return } soleil = { samples: <[Date, number][]>[], x: <string[]>[], y: <number[]>[], type: 'lines', line: { color: "#ffbf00", width: 1, }, name: "Production solaire", bucket: 1, } somme = { samples: <[Date, number][]>[], x: <string[]>[], y: <number[]>[], type: 'lines', line: { color: "#b81111", width: 1, }, name: "Consommation totale", bucket: 1, } let min_t = 0 let max_t = 0 let annee_ :Record<string, { t: number[], y: number[], i: number }> = {} for(let [prm, r] of Object.entries(annee)) { let d: { t: number[], x: string[], y: number[], i: number } = { t: [], x: [], y: [], i: 0, } for(let semaine of r) { let b = new DataView(base64decode(semaine).buffer) let length = b.getUint32(0, true) for(let i = 8; i < 8 + 8 * length; i+= 8) { let t = b.getUint32(i, true) let v = b.getUint32(i+4, true) if(!min_t || t < min_t) { min_t = t } if(t > max_t) { max_t = t } d.t.push(t) d.y.push(v) } } annee_[prm] = d } if(pr.length > 0) { if(min_t >= max_t) { let d = new Date() min_t = (new Date(d.getFullYear(), 0, 1)).getTime() / 1000 max_t = min_t + 364 * 24 * 3600 } for (let p of pr) { let d: { t: number[], y: number[], i: number } = { t: [], y: [], i: 1, } let i = 0 for(let t = min_t; t <= max_t; t += 1800) { d.t.push(t); let pp = profs.get(p.type) if(!pp) { profs.set(p.type, await profile(p.type)) } if(pp) { d.y.push(1000 * pp.coefs[i] * p.conso / pp.total) } i += 1 } annee_[p.type] = d } } auto = 0 totalProd = 0 totalConso = 0 let annuelle = await fetch("https://coturnix.fr/pvgis") let pvgis: {outputs: { hourly: {time: string, "G(i)": number, T2m: number, H_sun: number, Int: number}[]}} = await annuelle.json() const start = new Date(min_t * 1000) const janvier = new Date(start.getFullYear(), 0, 1) for(let t = min_t; t < max_t; t += 1800) { let tt = new Date(t*1000); let len = pvgis.outputs.hourly.length let n = ((t * 1000 - janvier.getTime()) / 3600000) % len let pp = 0 if(n == Math.floor(n)) { pp = pvgis.outputs.hourly[n].H_sun } else { let extra = n - Math.floor(n) pp = pvgis.outputs.hourly[Math.floor(n)]['G(i)'] * (1 - extra) + pvgis.outputs.hourly[Math.floor(n) + 1]['G(i)'] * extra } soleil.samples.push([ tt, pp * puissance ]) let y = 0 for(let [_, r] of Object.entries(annee_)) { while(r.i + 1 < r.t.length && r.t[r.i + 1] < t) { r.i += 1 } if(r.i < r.t.length) { if(!isNaN(r.y[r.i])) y += r.y[r.i] } } somme.samples.push([ tt, y ]) totalProd += pp * puissance totalConso += y auto += Math.min(y, pp * puissance) } console.log(totalProd, totalConso); if(!Plotly) //@ts-ignore Plotly = await import('plotly.js-dist') if(max_t > min_t) { redraw() } } function sample( s: { samples: [Date, number][], x: string[], y: number[], bucket: number }, w?: number, ) { console.log("sample", w, s); let ww = w || s.samples.length let n = 400 let bbox = (<SVGGraphicsElement | undefined>(document.getElementsByClassName("bglayer")[0]))?.getBBox() if(bbox && bbox.width >= 200) { n = bbox.width / 2 } s.bucket = Math.ceil(ww / n) console.log("bucket", s.bucket); s.x = []; s.y = []; for(let i = 0; i < s.samples.length; ) { const i0 = i; let sum = 0; let sz = Math.min(s.bucket, s.samples.length - i0) for(; i < i0 + s.bucket && i < s.samples.length; i++) { sum += s.samples[i][1] / sz } s.x.push(s.samples[i0][0].toLocaleString()) s.y.push(sum) } } function redraw() { sample(soleil) sample(somme) let layout = { title: "Autoconsommation", autosize: true, automargin: true, plot_bgcolor: "#fff0", paper_bgcolor: "#fff0", font: { color: darkMode ? '#fff': '#000' }, xaxis: { gridcolor: darkMode ? '#444': '#bbb', ticklabeloverflow: 'allow', ticklabelstep: 4, }, yaxis: { title: { text: 'kW' }, gridcolor: darkMode ? '#444': '#bbb', color: darkMode ? '#fff': '#000', }, } plotStyle = "min-height:450px" Plotly!.react('plot', [soleil, somme], layout, { responsive: true }); let relayout = function(event: any) { console.log(event) let w = event["xaxis.range[1]"] - event["xaxis.range[0]"] let layout_ = JSON.parse(JSON.stringify(layout)) if(isNaN(w)) { sample(soleil) sample(somme) console.log(layout_) layout_.xaxis.autorange = true layout_.yaxis.autorange = true plotStyle = "min-height:450px" Plotly!.react('plot', [soleil, somme], layout_, { responsive: true }) } else { let sb = somme.bucket sample(soleil, w) sample(somme, w) let sb_ = somme.bucket console.log("buckets", sb, sb_) layout_.xaxis.range = [ event["xaxis.range[0]"] * sb / sb_, event["xaxis.range[1]"] * sb / sb_, ] layout_.yaxis.autorange = true /*range = [ event["yaxis.range[0]"], event["yaxis.range[1]"], ]*/ console.log(layout_.xaxis) plotStyle = "min-height:450px" Plotly!.react('plot', [soleil, somme], layout_, { responsive: true }) } }; //@ts-ignore document.getElementById('plot')?.on('plotly_relayout', relayout) var dataProd = [{ values: [Math.round(auto), Math.round(totalProd - auto)], labels: ['Autoconsommé', 'Surplus'], marker: { colors: ["#ffbf00dd", "#b81111dd"] }, type: 'pie', sort: false, hole: .5, }]; var dataConso = [{ values: [Math.round(auto), Math.round(totalConso - auto)], labels: ['Autoproduit', 'Alloproduit'], marker: { colors: ["#ffbf00dd", "#b81111dd"] }, type: 'pie', sort: false, hole: .5, }]; plotStyle = "min-height:450px" Plotly.react('autoProd', dataProd, { title: "Autoconsommation", font: { color: darkMode ? '#fff': '#000' }, plot_bgcolor: "#fff0", paper_bgcolor: "#fff0", autosize: true, legend: { orientation: "h", } }, { responsive: true }); Plotly.react('autoConso', dataConso, { title: "Autoproduction", font: { color: darkMode ? '#fff': '#000' }, plot_bgcolor: "#fff0", paper_bgcolor: "#fff0", autosize: true, legend: { orientation: "h", } }, { responsive: true }); } </script> <form method="POST" action="/api/faisan" id="pdl" on:submit={refresh}> </form> <table class="table"> {#each pr as pr, i} <tr> <td class="px-3">Profil</td> <td> <select class="form-select" bind:value={pr.type}> {#each prt as p} <option value={p}>{p}</option> {/each} </select> </td> <td> <input type="number" class="form-control" placeholder="Consommation Annuelle (kWh)" bind:value={pr.conso}/> </td> <td style="width:1%;white-space:nowrap;"> <button class="btn btn-primary" on:click={(_ev) => delProfil(i)}><i class="bi bi-x-circle"/></button> </td> </tr> {/each} <tr> <td class="px-3" style="width: 25%">Liste de PDL, séparés par des virgules</td> <td colspan="3"><input class="form-control" form="pdl" id="prms" name="prms" bind:value={prms}/></td> </tr> </table> <div class="my-5 text-center"> <button class="btn btn-primary" on:click={addProfil}><i class="bi bi-plus-circle" /> Ajouter un profil</button> <button class="btn btn-primary" form="pdl">Simuler</button> </div> <div class="my-3"> <div class="mx-auto" style={plotStyle} id="plot"></div> </div> <div class="my-3 row"> <div class="col-12 col-sm-6 p-0" style={plotStyle} id="autoConso"></div> <div class="col-12 col-sm-6 p-0" style={plotStyle} id="autoProd"></div> </div>