<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>