<script lang="ts"> import { browser } from '$app/environment' import { onMount, tick } from 'svelte' import { assets as a } from '$app/paths' import type { Grain } from '../../../grain-types' import { base } from '$app/paths' import { sample, chooseScale, relayout } from '../../../plot' export let position: { lat: number; lng: number } | undefined export let azimuth: number | undefined export let inclinaison: number | undefined export let puissance: number export let prmsFeature: boolean = false export let id: string = '' export let prms: Grain.Prm[] = [] export let prodWeekly: number[] = [] export let maxProd: number = 0 export let assets = a as string export let server = 'https://coturnix.fr' let consentement = false function addPrm(_ev: Event) { prms.push({ prm: null, nom: '', prenom: '' }) consentement = false prms = prms } function delPrm(_ev: Event, i: number) { prms.splice(i, 1) prms = prms } let pvgis: null | { inputs: { location: { latitude: number; longitude: number } mounting_system: { fixed: { slope: { value: number }; azimuth: { value: number } } } } outputs: { hourly: { time: string 'G(i)': number P: number T2m: number H_sun: number Int: number }[] } } = null export let pvgis_loading = false let prm_loading = false let initialised = false $: { initialised && updatePlot(puissance, position, azimuth, inclinaison) } let Plotly: any let erreur = '' onMount(async () => { //@ts-ignore Plotly = await import('plotly.js-dist') await updatePlot(puissance, position, azimuth, inclinaison) 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 profils: Record<string, { profils: string[] }> = { 'Résidentiel ≤6kVA': { profils: ['RES1-P1'], }, 'Résidentiel 6>kVA≤36': { profils: ['RES11-P1'], }, 'Résidentiel ≤36kVA HPHC': { profils: ['RES2'], }, 'Professionel ≤36kVA': { profils: ['PRO1-P1'], }, 'Professionnel ≤36kVA HWE': { profils: ['PRO1WE'], }, 'Professionnel ≤36kVA HPHC': { profils: ['PRO2'], }, 'Entreprise 36<kVA<250 HPHC ETE-HIV': { profils: ['ENT1'], }, /* "Production hydraulique":{ "profils":[ "PRD1-P1" ] }, "Production cogénération":{ "profils":[ "PRD2-P1" ] }, "Production photovoltaïque":{ "profils":[ "PRD3-P1" ] }, */ } function addProfil(_ev: Event) { pr.push({ type: prt[0], conso: 0 }) pr = pr } async function delProfil(i: number) { pr.splice(i, 1) pr = pr await updatePlot(puissance, position, azimuth, inclinaison) } async function profile( name: string ): Promise<{ coefs: number[]; total: number }> { prm_loading = true await tick() let profiles = await Promise.all( profils[name].profils.map(async (x) => { let resp = await fetch(`${assets}/profiles/${x}.csv`) return await resp.text() }) ) prm_loading = false await tick() let annee = [] let total = 0 let file = 0 for (let p of profiles) { let l = 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 if (file == 0) { annee.push(c) } else { annee[l] += c } total += c } l += 1 } file += 1 } return { coefs: annee, total } } let annee: Record<number, number[]> = {} async function refresh(event: SubmitEvent) { event.preventDefault() if (prms) { prm_loading = true await tick() const resp = await fetch( `${server}/api/grain/projet/${id || 'nouveau'}/prms`, { method: 'POST', body: JSON.stringify(prms), } ) if (resp.ok) { annee = await resp.json() } prm_loading = false await tick() } else { erreur = 'La liste des PRMs est vide' } await updatePlot(puissance, position, azimuth, inclinaison) } let auto = 0 let totalProd = 0 let totalConso = 0 let soleil = { hourly: { x: <string[]>[], y: <number[]>[], }, daily: { x: <string[]>[], y: <number[]>[], }, weekly: { x: <string[]>[], y: <number[]>[], }, x: <string[]>[], y: <number[]>[], type: 'bar', line: { color: '#ffbf00', width: 1, }, marker: { color: '#ffbf00', width: 1, }, name: 'Production solaire', bucket: 1, } let somme = { hourly: { x: <string[]>[], y: <number[]>[], }, daily: { x: <string[]>[], y: <number[]>[], }, weekly: { x: <string[]>[], y: <number[]>[], }, x: <string[]>[], y: <number[]>[], type: 'bar', line: { color: '#b81111', width: 1, }, marker: { color: '#b81111', width: 1, }, name: 'Consommation totale', bucket: 1, } async function updatePv( puissance: number, position?: { lat: number; lng: number }, azimuth?: number, inclinaison?: number ) { let d = new Date() let min_t = new Date(d.getFullYear(), 0, 1).getTime() / 1000 let max_t = min_t + 364 * 24 * 3600 if ( !pvgis || (position !== undefined && pvgis.inputs.location.latitude != position?.lat) || (position !== undefined && pvgis.inputs.location.longitude != position?.lng) || (azimuth !== undefined && pvgis.inputs.mounting_system.fixed.slope.value != inclinaison) || (azimuth !== undefined && pvgis.inputs.mounting_system.fixed.azimuth.value != azimuth) ) { pvgis_loading = true await tick() let annuelle = await fetch(`${base}/pvgis`, { method: 'POST', body: JSON.stringify({ lat: position?.lat, lng: position?.lng, azimuth, inclinaison, }), }) pvgis = await annuelle.json() pvgis_loading = false await tick() } soleil.hourly = { x: [], y: [] } soleil.daily = { x: [], y: [] } soleil.weekly = { x: [], y: [] } maxProd = 0 const start = new Date(min_t * 1000) const janvier = new Date(start.getFullYear(), 0, 1) for (let t = min_t; t < max_t; t += 3600) { let tt = new Date(t * 1000) let tts = tt.toLocaleString() let tds = tt.toLocaleDateString() let pp = 0 if (pvgis) { let len = pvgis.outputs.hourly.length let n = ((t * 1000 - janvier.getTime()) / 3600000) % len const pvvar = 'P' pp = (pvgis.outputs.hourly[Math.floor(n)][pvvar] * puissance) / 1000 soleil.hourly.x.push(tts) soleil.hourly.y.push(pp) if ((t - min_t) % (24 * 3600) == 0) { if ((t - min_t) % (24 * 7 * 3600) == 0) { if (soleil.weekly.y.length) { maxProd = Math.max( maxProd, soleil.weekly.y[soleil.weekly.y.length - 1] ) } soleil.weekly.x.push(tds) soleil.weekly.y.push(pp) } else { soleil.weekly.y[soleil.weekly.y.length - 1] += pp } soleil.daily.x.push(tds) soleil.daily.y.push(pp) } else { soleil.daily.y[soleil.daily.y.length - 1] += pp soleil.weekly.y[soleil.weekly.y.length - 1] += pp } } } prodWeekly = soleil.weekly.y } async function updatePlot( puissance: number, position?: { lat: number; lng: number }, azimuth?: number, inclinaison?: number ) { if (!browser) { return } await updatePv(puissance, position, azimuth, inclinaison) let annee_: Record<string, { t: number[]; y: number[]; i: number }> = {} let d = new Date() const min_t = new Date(d.getFullYear(), 0, 1).getTime() / 1000 const max_t = min_t + 364 * 24 * 3600 for (let [prm, r] of Object.entries(annee)) { let d: { t: number[]; y: number[]; i: number } = { t: [], y: [], i: 0, } let i = 0 for (let t = min_t; t < max_t; t += 1800) { d.t.push(t) d.y.push(r[i] / 1000) i += 1 } annee_[prm] = d } if (pr.length > 0) { for (let p of pr) { let d: { t: number[]; y: number[]; i: number } = { t: [], y: [], i: 0, } 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((pp.coefs[i] * p.conso) / pp.total) } i += 1 } annee_[p.type] = d } } let auto_ = sample(min_t, max_t, annee_, somme, soleil) auto = auto_.auto totalProd = auto_.totalProd totalConso = auto_.totalConso if (!Plotly) //@ts-ignore Plotly = await import('plotly.js-dist') await tick() redraw() } let has_plot = { has_plot: false } let layout = {} function relayout_(event: any) { console.log('relayout_') const plot = document.getElementById('plot')! relayout(event, plot, Plotly, layout, has_plot, soleil, somme) } function redraw() { const plot = document.getElementById('plot') if (!plot) return chooseScale(soleil) chooseScale(somme) let unite: string if (soleil.bucket == 24) { unite = 'kWh/jour' } else if (soleil.bucket == 24 * 7) { unite = 'kWh/semaine' } else { unite = 'kWh' } 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, tickangle: 45, }, yaxis: { title: { text: unite }, gridcolor: darkMode ? '#444' : '#bbb', color: darkMode ? '#fff' : '#000', }, showlegend: true, legend: { xanchor: 'center', x: 0.5, y: -0.4, orientation: 'h', }, } plotStyle = 'min-height:450px' if (has_plot.has_plot) { Plotly!.update(plot, [soleil, somme], layout) } else { Plotly!.newPlot(plot, [soleil, somme], layout, { responsive: true, }) //@ts-ignore plot.on('plotly_relayout', relayout_) has_plot.has_plot = true } var dataProd = [ { values: [Math.round(auto), Math.round(totalProd - auto)], labels: ['Autoconsommé', 'Surplus'], marker: { colors: ['#ffbf00dd', '#b81111dd'] }, type: 'pie', sort: false, hole: 0.5, }, ] var dataConso = [ { values: [Math.round(auto), Math.round(totalConso - auto)], labels: ['Autoproduit', 'Fournisseur'], marker: { colors: ['#ffbf00dd', '#b81111dd'] }, type: 'pie', sort: false, hole: 0.5, }, ] plotStyle = 'min-height:450px' Plotly.react( 'autoProd', dataProd, { title: "Autoconsommation sur l'année", font: { color: darkMode ? '#fff' : '#000', }, plot_bgcolor: '#fff0', paper_bgcolor: '#fff0', autosize: true, legend: { orientation: 'h', }, }, { responsive: true } ) Plotly.react( 'autoConso', dataConso, { title: "Autoproduction sur l'année", font: { color: darkMode ? '#fff' : '#000', }, plot_bgcolor: '#fff0', paper_bgcolor: '#fff0', autosize: true, legend: { orientation: 'h', }, }, { responsive: true } ) } </script> <form method="POST" action="/api/grain" id="pdl" on:submit={refresh} /> {#if erreur} <div id="erreur" class="alert alert-danger fade d-flex" class:show={erreur} role="alert"> {erreur} <button type="button" class="btn-close" style="padding:20px;margin:-16px -16px -16px auto" on:click={() => (erreur = '')} aria-label="Close" /> </div> {/if} <h2 class="my-5">Profils de consommation</h2> {#if pr.length} <table class="table"> <thead ><tr><th /><th>Profil</th><th>Consommation annuelle</th><th /></tr ></thead> <tbody> {#each pr as pr, i} <tr> <td class="px-3 align-middle">Profil</td> <td> <select class="form-select" bind:value={pr.type} on:change={(_ev) => updatePlot( puissance, position, azimuth, inclinaison )}> {#each Object.entries(profils) as [p, _]} <option value={p}>{p}</option> {/each} </select> </td> <td> <input type="number" class="form-control" placeholder="Abonnement (kW)" min="0" step="500" bind:value={pr.conso} on:change={(_ev) => updatePlot( puissance, position, azimuth, inclinaison )} /> <!-- <input type="number" class="form-control" placeholder="Nombre de compteurs" min="0" step="1" bind:value={pr.nCompteurs} on:change={(_ev) => updatePlot(puissance, position, azimuth, inclinaison)} /> --> </td> <td style="width:1%;white-space:nowrap;text-align:center"> <button class="btn btn-link" on:click={(_ev) => delProfil(i)} ><i class="bi bi-x-circle text-primary" /></button> </td> </tr> {/each} </tbody> </table> {/if} <div class="text-center"> <button class="btn btn-primary" on:click={addProfil} ><i class="bi bi-plus-circle" /> Ajouter un profil</button> </div> {#if prmsFeature} <h2 class="my-5">Relevés de compteurs</h2> {#if prms.length} <table class="table"> <thead ><tr ><th >PRM<button type="button" class="btn btn-link align-baseline p-0 ms-2" data-bs-toggle="popover" data-bs-trigger="hover focus" data-bs-content="Un PRM, aussi appelé PDL, est l'identifiant d'un compteur électrique. On peut le trouver sur les factures d'électricité, ou en appuyant sur le bouton + du compteur Linky." ><i class="bi bi-question-circle" /></button ></th ><th>Prénom</th><th>Nom</th><th /></tr ></thead> <tbody> {#each prms as prm, i} <tr> <td ><input class="form-control" bind:value={prm.prm} type="number" /></td> <td ><input class="form-control" bind:value={prm.nom} /></td> <td ><input class="form-control" bind:value={prm.prenom} /></td> <td style="width:1%;white-space:nowrap;text-align:center"> <button class="btn btn-link" on:click={(ev) => delPrm(ev, i)} ><i class="bi bi-x-circle text-primary" /></button> </td> </tr> {/each} </tbody> </table> <div class="my-3 form-check"> <input class="form-check-input" type="checkbox" id="consentement" bind:checked={consentement} /> <label class="form-check-label" for="consentement" >Ces abonnés consentent à ce que j'accède à leurs données</label> </div> {/if} <div class="d-flex justify-content-center"> <button class="mx-2 btn btn-primary" on:click={addPrm} ><i class="bi bi-plus-circle" /> Ajouter un compteur</button> {#if prms.length} <button disabled={!consentement} class="mx-2 btn btn-primary" form="pdl">Mettre à jour</button> <div class="mx-2 spinner-border text-primary" class:d-none={!prm_loading} /> {/if} </div> {/if} <h2 class="mt-5">Simulation</h2> <table class="table table-bordered d-inline-block"> <tbody> <tr><td>Production totale:</td><td>{Math.round(totalProd)}kWh</td></tr> <tr ><td>Consommation totale:</td><td>{Math.round(totalConso)}kWh</td ></tr> </tbody> </table> {#if pvgis_loading} <div class="my-3 d-flex align-items-center text-primary"> <div class="spinner-border text-primary me-3" /> Chargement de la production solaire </div> {/if} <div class="my-3"> <div class="mx-auto" style={plotStyle} id="plot" /> </div> <div class="my-3 row" class:d-none={totalProd == 0 || totalConso == 0}> <div class="col-12 col-sm-6 p-0" style={plotStyle} id="autoProd" /> <div class="col-12 col-sm-6 p-0" style={plotStyle} id="autoConso" /> </div>