<script lang="ts"> import { browser } from '$app/environment' import { onMount, tick } from 'svelte' import { dev } from '$app/environment'; import { assets as a } from '$app/paths' import type { Writable }from 'svelte/store' import { get }from 'svelte/store' import { getContext } from 'svelte' import { Grain } from '../../../../grain-types' import { base } from '$app/paths' import { chooseScale, relayout, modeBarButtonsToRemove } from '../../../../plot' import type { Sample } from '../../../../plot' import type {Derived, PVGIS, PVGISInputs} from '../calcul' import * as calcul from '../calcul' export let data: { id: string, accesParticuliers: boolean, prms: Grain.Prm[], active: boolean, profils: Grain.Profil[], }; export let annee: Record<number, number[]> = {} console.log(data) export let assets: string = a; let s: Derived = getContext("simulation") const soleil: Writable<Sample> = getContext("soleil") const expert: Writable<boolean> = getContext("expert") let _communaute: Writable<(Grain.Communaute & {isMoving?: boolean}) | null> = getContext("communaute") const server = dev ? 'http://localhost:5173' : 'https://coturnix.fr' function addPrm(_ev: Event) { data.prms.push({ prm: null, nom: '', prenom: '', siret: '', pro: true }) data.prms = data.prms } const pvgis: Writable<{ pvgis: null | PVGIS, loading: boolean }> = getContext("pvgis") let pvgis_: null | PVGISInputs = null export let pvgis_loading = false let prm_loading = false let initialised = false let puissance = 0 let communaute: null | (Grain.Communaute & { isMoving?: boolean }) = null let Plotly: any let erreur = '' onMount(async () => { //@ts-ignore Plotly = await import('plotly.js-dist') s.p.puissance.subscribe((value) => { puissance = value initialised && updatePlot(puissance, communaute) }) _communaute.subscribe((value) => { communaute = value initialised && updatePlot(puissance, communaute) }) pvgis.subscribe((pvgis) => { console.log("pvgis updated") if(initialised && (pvgis.pvgis?.inputs != pvgis_)) { pvgis_ = pvgis.pvgis?.inputs || null updatePlot(puissance, communaute) } }) for(let p of data.prms) { p.consentement = false } initialised = true updatePlot(puissance, communaute) }) let plotStyle = '' 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_ = Grain.profils($expert) expert.subscribe((value) => profils_ = Grain.profils(value)) function addProfil(_ev: Event) { const prt = Object.keys(profils_) data.profils.push({ type: prt[0], conso: 0 }) data.profils = data.profils } async function delProfil(i: number) { data.profils.splice(i, 1) data.profils = data.profils await updatePlot(puissance, communaute) } const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); var lastRequest = performance.now() async function refresh(event: SubmitEvent) { event.preventDefault() if (data.prms) { prm_loading = true await tick() for(let i = 0; i < data.prms.length; i++) { var file = document.getElementById(`releve-${i}`) as HTMLInputElement | null; if (file?.files?.length) { var reader = new FileReader(); reader.readAsText(file.files[0], "UTF-8"); let r: string | undefined = await new Promise((resolve) => { reader.onload = function (evt) { console.log(typeof (evt.target?.result)) let result = evt.target?.result?.toString() if(result) { resolve(result) } else { resolve(undefined) } } }) data.prms[i].releve = r } let siret = document.getElementById(`siret-${i}`) as HTMLInputElement | null; if(siret?.value) { let now = performance.now(); // Rate-limiting: pas plus de 7 requêtes par seconde. if(now - lastRequest < 150) { await sleep(150 - (now - lastRequest)) } lastRequest = now const resp = await fetch( `https://recherche-entreprises.api.gouv.fr/search?q=${siret.value}` ) if(resp.status == 200) { type Ent = { results: { dirigeants: { nom: string, prenoms: string, }[], matching_etablissements: { libelle_voie: string, commune: string, }[] }[] } let ent: Ent = await resp.json() console.log(ent.results[0].matching_etablissements[0].commune) if(ent.results.length) { data.prms[i].nom = ent.results[0].dirigeants[0].nom data.prms[i].prenom = ent.results[0].dirigeants[0].prenoms data.prms[i].codeinsee = ent.results[0].matching_etablissements[0].commune console.log(data.prms[i]) } } } } console.log(data.prms); const resp = await fetch( `${server}/api/grain/projet/${data.id || 'nouveau'}/prms`, { method: 'POST', body: JSON.stringify(data.prms), } ) if (resp.ok) { annee = await resp.json() } prm_loading = false await tick() } else { erreur = 'La liste des PRMs est vide' } await updatePlot(puissance, communaute) } let auto = 0 let totalProd = 0 let totalConso = 0 let somme: Sample = { hourly: { x: [], y: [], }, daily: { x: [], y: [], }, weekly: { x: [], y: [], }, x: [], y: [], type: 'bar', line: { color: '#b81111', width: 1, }, marker: { color: '#b81111', width: 1, }, name: 'Consommation totale', bucket: 1, } async function updatePv( puissance: number, communaute: (Grain.Communaute&{isMoving?: boolean}) | null, ) { if(communaute?.isMoving) { return pvgis } pvgis_loading = true await calcul.updatePv(puissance, communaute, pvgis, soleil, base) pvgis_loading = false await tick() } async function updatePlot( puissance: number, communaute: (Grain.Communaute & { isMoving?: boolean }) | null, ) { console.log("isMoving", communaute?.isMoving) if(communaute?.isMoving) { return } if (!browser) { return } await updatePv(puissance, communaute) console.log("updated") prm_loading = true; let auto_ = await calcul.updatePlot( assets, annee, data.profils, profils_, somme, soleil, ) prm_loading = false; auto = auto_.auto totalProd = auto_.totalProd totalConso = auto_.totalConso console.log("updated auto", auto, totalProd, totalConso) if (!Plotly) //@ts-ignore Plotly = await import('plotly.js-dist') 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, get(soleil), somme) } function redraw() { console.log("redraw") const plot = document.getElementById('plot') if (!plot) return let soleil_ = get(soleil) 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, tickformat: soleil_.bucket < 24 ? '%x %X': '%x', type: 'datetime', }, 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', }, } if (has_plot.has_plot) { Plotly!.update(plot, [soleil_, somme], layout) } else { Plotly!.newPlot(plot, [soleil_, somme], layout, { responsive: true, modeBarButtonsToRemove: modeBarButtonsToRemove, }) //@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, }, ] if(totalProd && totalConso) { 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, automargin: 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, automargin: true, legend: { orientation: 'h', }, }, { responsive: true } ) } else { plotStyle = '' } } </script> <div class="tab-content" id="simuTabContent"> <div class="tab-pane fade active show p-3" id="simu-tab-pane" role="tabpanel" aria-labelledby="simu-tab" tabindex="0"> <h3 class="my-3 fs-1 fw-bold">Production… <span class="text-primary">Consommation</span></h3> <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 data.profils.length} {#if $expert} <div>Vous êtes en mode expert. Dans ce mode, vous avez un accès direct à tous les profils utilisés par les opérateurs du réseau pour simuler les consommations. Voir <a href="https://www.enedis.fr/responsable-dequilibre-profilage-et-profils">la documentation</a>.</div> {/if} <table class="table"> <thead ><tr><th /><th>Profil</th><th>Consommation annuelle (kWh)</th><th /></tr ></thead> <tbody> {#each data.profils 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, communaute, )}> {#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, communaute, )} /> </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> <h2 class="my-5">Relevés de compteurs</h2> {#if data.active && data.id && data.id != 'nouveau'} <div class="mb-5">Vous pouvez ajouter des compteurs d'entreprises ou d'associations par leur numéro de compteur et leur numéro de SIRET. </div> {#if data.prms.length} {#each data.prms as prm, i} <div class="row my-2"> <div class="col-3 d-flex align-middle"> <label class="ms-auto form-label" for="prm-{i}">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> </label> </div> <div class="col d-flex align-items-center"> <input class="form-control form-control-sm" id="prm-{i}" bind:value={prm.prm} on:keydown={(e) => (e.key == 'ArrowUp' || e.key == 'ArrowDown') && e.preventDefault() } type="number" /> </div> <div class="col col-auto d-flex align-items-center"> <button class="btn btn-outline-primary btn-sm" on:click={(e) => { e.preventDefault(); data.prms.splice(i, 1); data.prms = data.prms; }} ><i class="bi bi-trash"/></button> </div> </div> <div class="row my-2"> <div class="col-3 d-flex align-middle"> <label class="ms-auto form-label" for="prm-{i}">Relevé au format CSV</label> </div> <div class="col d-flex align-items-center"> <input class="form-control form-control-sm" id="releve-{i}" type="file" /> {#if prm.releve?.length} <i class="ms-3 bi bi-check-circle"/> {/if} </div> </div> <div class="row my-2"> <div class="col-9 ms-auto"> <input type="radio" class="btn-check" id="particulier" value={false} bind:group={prm.pro}> <label class="btn btn-sm" for="particulier">Particulier</label> <input type="radio" class="btn-check" id="pro" value={true} bind:group={prm.pro}> <label class="btn btn-sm" for="pro">Professionnel</label> </div> </div> {#if prm.pro} <div class="row my-2"> <div class="col-3 d-flex text-end align-items-center"> <label class="ms-auto form-label" for="siret-{i}">SIRET du titulaire du compteur</label> </div> <div class="col-9 d-flex align-items-center"> <input class="form-control form-control-sm" id="siret-{i}" bind:value={prm.siret} /> </div> </div> <div class="row my-2"> <div class="col-3 d-flex text-end align-items-center"> <label class="ms-auto form-label" for="prenom-{i}">Prénom du titulaire du compteur</label> </div> <div class="col-9 d-flex align-items-center"> <input class="form-control form-control-sm" id="prenom-{i}" bind:value={prm.prenom} /> </div> </div> <div class="row my-2"> <div class="col-3 d-flex text-end align-items-center"> <label class="ms-auto form-label" for="nom-{i}">Nom du titulaire du compteur</label> </div> <div class="col-9 d-flex align-items-center"> <input class="form-control form-control-sm" id="nom-{i}" bind:value={prm.nom} /> </div> </div> {:else} <div class="row my-2"> <div class="col-3 d-flex text-end align-items-center"> <label class="ms-auto form-label" for="prenom-{i}">Prénom du titulaire du compteur</label> </div> <div class="col-9 d-flex align-items-center"> <input class="form-control form-control-sm" id="prenom-{i}" bind:value={prm.prenom} /> </div> </div> <div class="row my-2"> <div class="col-3 d-flex text-end align-items-center"> <label class="ms-auto form-label" for="nom-{i}">Nom du titulaire du compteur</label> </div> <div class="col-9 d-flex align-items-center"> <input class="form-control form-control-sm" id="nom-{i}" bind:value={prm.nom} /> </div> </div> <div class="row my-2"> <div class="col-3 d-flex text-end align-items-center"> <label class="ms-auto form-label" for="numerorue-{i}">Numéro et rue</label> </div> <div class="col-9 d-flex align-items-center"> <input class="form-control form-control-sm" id="numerorue-{i}" bind:value={prm.numerorue} /> </div> </div> <div class="row my-2"> <div class="col-3 d-flex text-end align-items-center"> <label class="ms-auto form-label" for="codepostal-{i}">Code INSEE de la commune</label> </div> <div class="col-9 d-flex align-items-center"> <input class="form-control form-control-sm" id="codepostal-{i}" bind:value={prm.codeinsee} /> </div> </div> <div class="row my-2"> <div class="col-3 d-flex text-end align-items-center"> <label class="ms-auto form-label" for="serie-{i}">Numéro de série</label> </div> <div class="col-9 d-flex align-items-center"> <input class="form-control form-control-sm" id="serie-{i}" bind:value={prm.serie} /> </div> </div> {/if} <div class="my-3 form-check d-flex"> <input class="form-check-input ms-auto me-2" type="checkbox" id="consentement-{i}" bind:checked={prm.consentement} /> <label class="form-check-label me-auto" for="consentement-{i}">Cet abonné consent à ce que j'accède à ses données.</label> </div> <hr class="my-4" /> {/each} <div class="my-3"> Ce service ne consulte la courbe de charge du compteur communiquant qu'une seule fois, et ce consentement expire dès que ces données pour l'année écoulée sont récupérées. Les données collectées depuis les compteurs communiquants sont stockées temporairement sur nos serveurs pour des raisons de performance, ne sont utilisées que pour afficher cette page et produire le rapport, et ne sont jamais conservées plus de 30 jours. </div> <div class="my-3"> Les données envoyées via un fichier sont conservées jusqu'à ce que vous supprimiez ce projet. </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 data.prms.length} <button disabled={!!data.prms.find((p) => !p.releve?.length && !p.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> {:else if data.active} <div class="my-3"> Sauvegardez ce projet pour avoir accès aux relevés de compteurs détaillés. </div> {:else} <div class="my-3"> Vous devez être abonné pour avoir accès aux relevés de compteurs détaillés. </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="min-height:450px" id="plot" /> </div> <div class="row"> <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> </div> </div> <style> input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } input[type=number] { appearance: none; -moz-appearance:textfield; /* Firefox */ } </style>