3O3IMVFKGSXMVLPLZR3H6MIG3EE5PS4632QE4JMK5SVX2PVSSKDAC
W2W6K6FJPLY7VH6YJPJQ7G5SLNFTWT4DM3V4MKGMEQTFRNDE3RIQC
LXNHFIWZPD6FZQABBKSOVB2CF4LO4BIA5D6DK7LX2TI63LUKX5LQC
GNIA6BK2TG2WOBCSLDBMX4MQ26YMFTIYQFH2XPSKWOBMWSUWOQIAC
CF23CLXE24SNMD7YLQ4OHNNVIRIK4FY25Z2Q5N42B7IDLINPE44QC
FHLPOB7EDQCT2WVTM7LIJTOLMZYZ6YWZ6RTQNZQFY3RXZP25VWBAC
5Y4B4NC65MPGP63IOCQRQYAFA7SYMIFSKIH63JLXKX7VII3EKKLQC
O22SKNQ6LUA45DZ7QFM3RFAMC2X64WMRITU4N5K22CWNGHIYFLJAC
NPDXW2TQMOKQL233633EHOJX4VYQGY5IVIHTRFLZSPG32CNX3XXAC
6NRS3ZPPZ6A42IEDMRLQ7LZ5TPPLFQ6AGYVS32TQRGHBIJQW4WBAC
DUF23XHRNSXFD7FSO3TSBI7LLUIKBSR6GELWNNJWOZV4YWWFEXCAC
6NB2PU2F64XDSURKYMBECKEN7ZQ63RMDN2GL5CP74YA6HE4K3TTAC
7MCTB5G2W6IB7JQXGO2HCMRHBUYIOKXI4MBODKLAJIK66EMLC5ZQC
UWD234SIIBCA45TXXSOSZEWLUF66NFPWET257C3WZQYX3L5EJPCAC
PIULOOUAM6OIXXSEIAETIVEEEIQ45DEQXB6QHTDSNVQDH57TGTUAC
J47EV2H73JKATJDKBZZNSIGAPC7G5X5DCWZ7YDN55XUNPYPS54TQC
AFTBFAXJMQ6SFDHO3X4725EQG64BVQYMTXRPFV6JO6IWCQSI5T3QC
UPEYNW7CF54F2XHS3SZ7NDZTFAUGJL4OJ6FHLKC7M6XLXL3M2L4QC
KNH5OGCTL3YCOQQPA32M76SGWHGHAPQRIZ7S2CBC5DGIEV32SOYQC
P7KZ25R4CNS4PZITRWPBVXRFSLY5TKCOFO6U3SDKX45ITNVU773QC
O63TXA57OHFIPUDT4QQBDVXRQA64CUIT7XU4G6MSQPMDRFJPM7IAC
O4ALNQWBOXUOZRVT6J7C64I4BNJMHIO44ZWNEWIOUBCLE2C7JBAQC
3WWWPCD6BDUHGDTIQHSVUSETNZCM56FYV5EDE2XIH3BNSXBUIKLQC
LX2ZVNAYX2FRCEIJ6XBCBILB3O7YT52AP56QDWCCGLATFCEB7KHQC
5B42RKMDCXT74BWXB7OVBMWVAEZAVFKV6T4I3KBESZIWSNQLPQCAC
XTA7M7PSEN3PURH4ARXUMZ3PMEQRVX7FPRQA6LANFVI776LJGELQC
QHVOZBISIQ3E3P5F6GGKWVDSXC4OA23HCIH2OMXEVGXKSF5W4APAC
DB6B3MEDBTEEIS6KOSJH7L5JFXKDMATJARZ6AR7TF2HCCKTFRZJQC
UPNRAOJN5TKYIAQIWTX2MURHUEIPHA2MJKAQOFCDL2PQSGK35OCQC
C56DYYC72SFNR6BSKCOVDY2QW6XNP66EUGGKCBROTCFJVQDB4FUAC
5FNNIOUNDS5SG75A4ZR6VY7DS6WU2GNNCOSSHX34RFTRB7LJ3D2QC
B2BGMBKLV2KPOSQH4ZVQAT7H4LKATWHCGGDWDLNPDJ5RP7V2FNZAC
PI6D6Y5EHSXHSILBYHKRKXWXZ6H6JOR5ZTP44CXBW4LUUIREY7OQC
XTPX4PYLPGJAEK52LYOYHLVRCL7N5FFAEY4E3OL23LOWLYPLCILQC
E6LXIE7VBUGCR762DFTHYZISEWYFV5E3NM3MGLT5Z7YFQQ5GTYRQC
V3QTRVNR5EVDZ2O2WRK7ZCVKSGGYBSNILH44OWXKOJK2ME5JC7UQC
FO4ZIKFIZY35GBBNL2V4OCRUSSI2WNZFWSZHAI5BWZENFH5QFG2AC
FERLNXU2QISSKOUBSPABB2ERHOWHQRI2GVKDJ352Y4B2FUXBRGRQC
5FPD6WH3Z6WVWVHWSU276I2JF7VRJZJB7GC76UY6MDRFJ7YGUMCQC
KSMN5QWFJ5SYMVDJQH7D5S6ZQ2YOPLNLTIYHN7LSMF5Y4U2URDAQC
5R7SS5AWSCPT4WWCS5WUBRFULLXATBZW4FFPBT6S5EYJVEWKL5NQC
3DZWNZUZGMHJH3JL3H7KN746C6XVNBRBFHBSVSB5AH7YYUFVYVDAC
YLOKW6ERT2XEQG4T6GBPSBCQWVIHWAKBESCXJBKMQBSDXV3C2ACQC
PI4VBCLQL4EGBUJ7B2FA3GH25B22CUIOMOZZ7GNPYCS5OSLIRKPAC
TLRGOVFX6KIWQSWRPXS73WW4EHJQ5L5W5NYMO6UM2EF2H3EPAHHQC
64D3E3ZOPPF3UFDFYWGBCKS5NTBR34J27FGESDZSSYMUEPD5ZMLQC
Y62XTAEHAC5MNJELNKACRLEVBUU2MKYKFPHB5M2S5X46B4EI2JJQC
R5TAB7OOEDIU75AKTUZMU2PDNQTENUYQ3UMWDRVQF7SYHLHEEXKAC
"types": ["@cloudflare/workers-types"]
}
"types": ["@types/bootstrap"]
},
"include": [
".svelte-kit/ambient.d.ts",
".svelte-kit/types/**/$types.d.ts",
"./vite.config.js",
"./vite.config.ts",
"./src/**/*.js",
"./src/**/*.ts",
"./src/**/*.svelte"
],
"exclude": [
"./node_modules/**",
".svelte-kit/[!ambient.d.ts]**",
"./src/service-worker.js",
"./src/service-worker.ts",
"./src/service-worker.d.ts"
]
interface Env {}
export class User {
env: Env
state: DurableObjectState
constructor(state: DurableObjectState, env: Env) {
this.state = state
this.env = env
}
async fetch(request: Request): Promise<Response> {
console.log(`parsed ${JSON.stringify(json)}`)
return await this.fetchCmd(json)
}
}
let id = crypto.randomUUID()
}
if (x) {
x.porteur = json.SaveProjet.porteur || x.porteur
x.tel = json.SaveProjet.tel || x.tel
x.adresse = json.SaveProjet.adresse || x.adresse
x.simulation = json.SaveProjet.simulation || x.simulation
}
}
)
resp.id = json.GetProjet
}
return new Response(JSON.stringify(resp || null), { headers })
}
}
}
throw new Error(exhaustiveCheck)
}
let opts = {
}
result.push({
})
}
return result
}
}
id: k.slice(3),
puissance: v.simulation.puissance,
t: v.t,
nom: v.nom,
let result: Grain.ListProjetsResp = []
for (let [k, v] of keys.entries()) {
console.log(k, v)
if (!v.nom) {
await this.state.storage.delete(k)
continue
}
let keys: Map<
string,
{
id: string
simulation: Grain.SimulationCommunaute
t: number
}
> = await this.state.storage.list(opts)
nom: string
prefix: 'pi.',
async listprojets(_from?: number): Promise<Grain.ListProjetsResp> {
const exhaustiveCheck: never = json
return new Response('null', { headers })
if ('DelProjet' in json) {
let t: undefined | { t: string } = await this.state.storage.get(
`pi.${json.DelProjet.id}`
)
if (t) {
await this.state.storage.delete(`pi.${json.DelProjet.id}`)
console.log('resp', JSON.stringify(resp))
if (resp) {
if ('GetProjet' in json) {
let resp = <Grain.GetProjetResp>(
await this.state.storage.get(`pi.${json.GetProjet}`)
return new Response('null', { headers })
await this.state.storage.put(`pi.${json.SaveProjet.id}`, x)
} else {
console.log('no such project')
x.nom = json.SaveProjet.nom || x.nom
if ('SaveProjet' in json) {
let x: undefined | Grain.GetProjetResp =
await this.state.storage.get(`pi.${json.SaveProjet.id}`)
porteur: json.CreateProjet.porteur,
tel: json.CreateProjet.tel,
adresse: json.CreateProjet.adresse,
simulation: json.CreateProjet.simulation,
t: Date.now(),
})
console.log('saved', id)
return new Response(JSON.stringify(id), { headers })
await this.state.storage.put(`pi.${id}`, {
nom: json.CreateProjet.nom,
if ('CreateProjet' in json) {
// Grain
if ('ListProjets' in json) {
return new Response(JSON.stringify(await this.listprojets()), {
headers,
})
async fetchCmd(json: Grain.Cmd): Promise<Response> {
let headers = new Headers()
headers.set('Content-Type', 'application/json')
let json = <Grain.Cmd>await request.json()
console.log('parsing json')
export default {}
import type { Grain } from './helpers'
<script lang="ts">
</script>
<div class="form-check form-switch">
<label class="form-check-label" for="annuel">Présentation annuelle</label>
</div>
{#if annuel}
<div class="my-3">
<thead>
<tr>
<th>Capital restant dû</th>
<th>Principal</th>
<th>Intérêts</th>
</tr>
</thead>
<tbody>
{#each remboursementAnnuel as r, periode}
<tr>
<td>{periode}</td>
<td>{crdAnnuel[periode].toFixed(0)}</td>
<td>{r.toFixed(0)}</td>
<td>{interetsAnnuels[periode].toFixed(0)}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{:else}
<div class="my-3">
<thead>
<tr>
<th>Mois</th>
<th>Capital restant dû</th>
<th>Principal</th>
<th>Intérêts</th>
</tr>
</thead>
<tbody>
{#each remboursement as r, periode}
<tr>
<td>{periode}</td>
<td>{crd[periode].toFixed(0)}</td>
<td>{r.toFixed(0)}</td>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</style>
<style lang="scss">
table {
font-size: 90%;
}
th {
font-size: 1rem;
font-weight: bold;
}
td {
padding: 5px 5px;
padding: 5px 7px 5px 10px;
text-align: end;
border: none;
}
<td
>{#if periode > 0}
{interets[periode].toFixed(0)}
{/if}
<table class="table table-striped border-0 d-inline-block">
<th>Année</th>
<table class="table table-striped border-0 d-inline-block banque">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="annuel"
bind:checked={annuel} />
let annuel = true
crd[0] = dette
remboursement[0] = 0
interets[0] = 0
crdAnnuel[0] = dette
remboursementAnnuel[0] = 0
interetsAnnuels[0] = 0
for (let i = 1; i <= duree; i++) {
if (i <= periodeRemboursementCapital) {
remboursement[i] = 0
} else if (i < nombreEcheancesPeriodeTronquee) {
remboursement[i] = ppmt(
i - periodeRemboursementCapital,
totalPeriodes,
-dette
)
} else if (i == nombreEcheances) {
remboursement[i] = crd[i - 1]
} else {
remboursement[i] = 0
}
crd[i] = crd[i - 1] - remboursement[i]
if (i % nombrePeriodesParAn == 0) {
crdAnnuel[i / nombrePeriodesParAn] = crd[i]
remboursementAnnuel[i / nombrePeriodesParAn] = 0
interetsAnnuels[i / nombrePeriodesParAn] = 0
}
remboursementAnnuel[Math.floor(i / nombrePeriodesParAn)] +=
remboursement[i]
interetsAnnuels[Math.floor(i / nombrePeriodesParAn)] += interets[i]
}
}
interets[i] = crd[i - 1] * taux_ * prorataPeriodique
taux_ * prorataPeriodique,
remboursement = new Array(duree + 1)
crd = new Array(duree + 1)
interets = new Array(duree + 1)
remboursementAnnuel = new Array(dureeAns_)
crdAnnuel = new Array(dureeAns_)
interetsAnnuels = new Array(dureeAns_)
// X11
let periodeRemboursementCapital = dureeDifferee
// W11
let totalPeriodes = dureeAns * nombrePeriodesParAn - dureeDifferee
// V9
let nombreEcheances = Math.ceil(nombreEcheancesPeriodeTronquee)
// V10
let nombreEcheancesPeriodeTronquee = dureeAns * nombrePeriodesParAn
$: {
dureeAns = duree / nombrePeriodesParAn
dureeAns_ = Math.ceil(dureeAns)
prorataPeriodique = 1 / nombrePeriodesParAn
export let remboursement = new Array(duree + 1)
export let remboursementAnnuel = new Array(dureeAns_)
export let crd = new Array(duree + 1)
export let crdAnnuel = new Array(dureeAns_)
export let interets = new Array(duree + 1)
export let interetsAnnuels = new Array(dureeAns_)
// V6
let dureeAns = duree / nombrePeriodesParAn
let dureeAns_ = Math.ceil(dureeAns)
let taux_ = Math.min(taux, 1)
let prorataPeriodique = 1 / nombrePeriodesParAn
// V8
export let nombrePeriodesParAn = 12
// Z7
export let dureeDifferee = 1
// Z8
export let duree: number
// V4
export let taux: number
// V3
export let dette: number
import { ppmt } from 'financial'
<script lang="ts">
</script>
<div class="chart-container">
<!-- Dots (if enabled) -->
{#if showDots && !dotInfo}
{#each I as i}
<circle
cx={xScale(xVals[i])}
cy={yScale(yVals[i])}
stroke={colors[colorVals[i]]}
</g>
{/each}
{/if}
<!-- Chart lines -->
{#each lines as subsetLine, i}
{#if dotInfo && false}
{:else}
{/if}
</g>
{/each}
<!-- Y-axis and horizontal grid lines -->
{#each yTicks as tick}
<g class="tick" transform="translate(0, {yScale(tick)})">
{#if horizontalGrid}
{/if}
<text x="-10" y="5">{tick + yFormat}</text>
</g>
{/each}
<text text-anchor="middle" x="0" y={marginTop - 10}>{yLabel}</text>
</g>
<!-- X-axis and vertical grid lines -->
{#each xTicks as tick, i}
<g class="tick" transform="translate({xScale(tick)}, 0)">
{#if verticalGrid}
{/if}
</g>
{/each}
</g>
{#each pointsScaled as point, i}
<path
stroke="none"
fill-opacity="0"
class="voronoi-cell"
d={voronoiGrid.renderCell(i)}
{/each}
</svg>
</div>
<!-- Tooltip -->
{#if dotInfo}
{subsets ? subsets[points[dotInfo[1]].color] : ''}:
</div>
{/if}
<style>
</style>
.tooltip {
border-radius: 5px;
padding: 5px;
box-shadow: rgba(0, 0, 0, 0.4) 0px 2px 4px,
rgba(0, 0, 0, 0.3) 0px 7px 13px -3px,
rgba(0, 0, 0, 0.2) 0px -3px 0px inset;
opacity: 1;
}
@media (prefers-color-scheme: dark) {
.y-axis text {
fill: white;
}
.x-axis text {
fill: white;
}
.tick-start {
stroke: white;
}
.tick-grid {
stroke: white;
}
.tick text {
fill: white;
}
.domain {
stroke: white;
}
}
.y-axis .tick text {
text-anchor: end !important;
}
.chart-container {
justify-content: center;
align-items: center;
margin-top: 50px;
margin-left: 8 0px;
}
svg {
max-width: 100%;
height: auto;
height: 'intrinsic';
margin: auto;
}
path {
fill: 'green';
}
.y-axis {
font-size: '10px';
font-family: sans-serif;
text-anchor: 'end';
}
.x-axis {
font-size: '10px';
font-family: sans-serif;
text-anchor: 'end';
}
.tick text {
font-size: 80%;
fill: white !important;
}
.tick {
opacity: 1;
}
.tick-start {
stroke: black;
stroke-opacity: 1;
}
.tick-grid {
stroke: black;
stroke-opacity: 0.2;
font-size: '11px';
color: black;
}
.tick text {
fill: black;
}
{new Date(points[dotInfo[1]].x * 1000).toLocaleString()}: {points[
dotInfo[1]
].y.toFixed(2)}{yFormat}
<div
class="tooltip"
style="position:absolute; left:{dotInfo[2].pageX +
12}px; top:{dotInfo[2].pageY +
12}px; pointer-events:none; background-color:{tooltipBackground}; color:{tooltipTextColor}">
on:mouseover={(e) => (dotInfo = [point, i, e])}
on:focus={(e) => (dotInfo = [point, i, e])} />
<text x={width - marginLeft - marginRight - 40} y={marginBottom}
>{xLabel}</text>
<text font-size="8px" x={-marginLeft / 4} y="20"
>{xTicksFormatted[i] + xFormat}</text>
<line
class="tick-grid"
y2={-height + insetTop + marginTop} />
<line class="tick-start" stroke="black" y2="6" />
<g
class="x-axis"
transform="translate(0,{height - marginBottom - insetBottom})"
pointer-events="none">
<path
class="domain"
stroke="black"
d="M{marginLeft},0.5 H{width - marginRight}" />
<line
class="tick-grid"
x1={insetLeft}
x2={width - marginLeft - marginRight} />
<line
class="tick-start"
x1={insetLeft - 6}
x2={insetLeft} />
<g
class="y-axis"
transform="translate({marginLeft}, 0)"
pointer-events="none">
<path
class="domain"
stroke="black"
d="M{insetLeft}, {marginTop} V{height - marginBottom + 6}" />
<path
class="line"
fill="none"
stroke={colors[i]}
d={subsetLine}
stroke-opacity={strokeOpacity[i]}
stroke-width={strokeWidth}
stroke-linecap={strokeLinecap}
stroke-linejoin={strokeLinejoin} />
<path
class="line"
fill="none"
stroke-opacity={points[dotInfo[1]].color === i
? '1'
: '0.1'}
stroke={colors[i]}
d={subsetLine}
stroke-width={strokeWidth}
stroke-linecap={strokeLinecap}
stroke-linejoin={strokeLinejoin} />
<circle
cx={xScale(points[dotInfo[1]].x)}
cy={yScale(points[dotInfo[1]].y)}
{r}
stroke={colors[points[dotInfo[1]].color]}
fill={dotsFilled} />
<g class="chartlines" pointer-events="none">
fill={dotsFilled ? colors[colorVals[i]] : 'none'} />
{r}
<g class="dot" pointer-events="none">
<svg
{width}
{height}
viewBox="0 0 {width} {height}"
cursor="crosshair"
on:mouseout={() => (dotInfo = null)}
on:blur={() => (dotInfo = null)}>
const xTicks = xScale.ticks(xScalefactor)
const xTicksFormatted = xTicks.map((el) =>
new Date(el * 1000).toLocaleDateString()
)
const yTicks = niceY.ticks(yScalefactor)
const pointsScaled = points.map((el) => [
xScale(el.x),
yScale(el.y),
el.color,
])
const delaunayGrid = Delaunay.from(pointsScaled)
const voronoiGrid = delaunayGrid.voronoi([0, 0, width, height])
$: {
lines = []
colors.forEach((_color, j) => {
const filteredI = I.filter((_el, i) => colorVals[i] === j)
lines.push(chartLine(filteredI))
})
}
const chartLine = line()
.defined((i) => cleanData[i])
.curve(curve)
.x((i) => xScale(xVals[i]))
.y((i) => yScale(yVals[i]))
const xDomain = [xVals[0], xVals[xVals.length - 1]]
const yDomain = [0, Math.max(...yVals)]
const xScale = xType(xDomain, xRange)
const yScale = yType(yDomain, yRange)
const niceY = scaleLinear()
.domain([0, Math.max(...yVals)])
.nice()
const I = range(xVals.length)
const gaps = (_d, i) => !isNaN(xVals[i]) && !isNaN(yVals[i])
const cleanData = points.map(gaps)
// For a single set of data
console.log(data)
if (!('data' in data[0])) {
x = Object.keys(data[0])[0]
y = Object.keys(data[0])[1]
xVals = data.map((el: Record<string, any>) => el[x])
yVals = data.map((el: Record<string, any>) => el[y])
colorVals = data.map((_) => 0)
points = data.map((el) => ({
x: el[x],
y: el[y],
color: 0,
}))
}
// For data with subsets (NOTE: expects 'id' and 'data' keys)
else {
x = Object.keys(data[0]?.data[0])[0]
y = Object.keys(data[0]?.data[0])[1]
data.forEach((subset, i) => {
subset.data.forEach((coordinate) => {
xVals.push(coordinate[x])
yVals.push(coordinate[y])
colorVals.push(i)
points.push({
x: coordinate[x],
y: coordinate[y],
color: i,
})
})
subsets.push(subset.id)
})
}
let x: string,
y: string,
dotInfo,
lines,
xVals = [],
yVals = [],
points = [],
subsets = [],
colorVals = []
const marginTop = 100 // the top margin, in pixels
const marginRight = 0 // the right margin, in pixels
const marginBottom = 30 // the bottom margin, in pixels
const marginLeft = 100 // the left margin, in pixels
const inset = 0 // inset the default range, in pixels
const width = 600 // the outer width of the chart, in pixels
const height = 400 // the outer height of the chart, in pixels
const xLabel = '' // a label for the y-axis
const yLabel = 'Consommation (Wh)' // a label for the y-axis
const xFormat = '' // a format specifier string for the y-axis
const yFormat = 'Wh' // a format specifier string for the y-axis
const horizontalGrid = true // show horizontal grid lines
const verticalGrid = true // show vertical grid lines
const colors = ['#b81111', '#ffbf00'] // fill color for dots && number of colors in fill array MUST match number of subsets in data
const showDots = false // whether dots should be displayed
const dotsFilled = true // whether dots should be filled or outlined
const r = 0 // (fixed) radius of dots, in pixels
const strokeWidth = 1 // stroke width of line, in pixels
const strokeOpacity = [0.8, 0.2, 0.8, 0.8] // stroke opacity of line
const tooltipBackground = 'white' // background color of tooltip
const tooltipTextColor = 'black' // text color of tooltip
const strokeLinecap = 'round' // stroke line cap of the line
const strokeLinejoin = 'round' // stroke line join of the line
const xScalefactor = width / 150 //y-axis number of values
const yScalefactor = height / 40 //y-axis number of values
const curve = curveLinear // method of interpolation between points
const xType = scaleUtc // type of x-scale
const insetTop = 50 // inset from top
const insetRight = inset // inset from right
const insetBottom = inset // inset fro bottom
const insetLeft = inset // inset from left
const xRange = [marginLeft + insetLeft, width - marginRight - insetRight] // [left, right]
const yType = scaleLinear // type of y-scale
const yRange = [height - marginBottom - insetBottom, marginTop + insetTop] // [bottom, top]
export let data
//@ts-nocheck
import {
line,
curveLinear,
Delaunay,
range,
scaleLinear,
scaleUtc,
} from 'd3'
// import data from './line-data';
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'
import { chooseScale, relayout, modeBarButtonsToRemove } from '../../../../plot'
import type {Derived, PVGIS} from '../calcul'
import * as calcul from '../calcul'
export let data: {
id: string,
accesParticuliers: boolean,
prms: Grain.Prm[],
guest?: boolean,
profils: Grain.Profil[],
};
console.log(data)
export let assets: string = a;
let s: Derived = getContext("simulation")
let expert: Writable<boolean> = getContext("expert")
let _communaute: Writable<(Grain.Communaute & {isMoving?: boolean}) | null> = getContext("communaute")
let prmsFeature: boolean = !data.guest
const server = dev ? 'http://localhost:5173' : 'https://coturnix.fr'
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
$: {
initialised && updatePlot(puissance, position, azimuth, inclinaison)
}
let puissance = 0
let communaute: null | (Grain.Communaute & { isMoving?: boolean }) = null
await updatePlot(puissance, position, azimuth, inclinaison)
s.p.puissance.subscribe((value) => {
puissance = value
initialised && updatePlot(puissance, communaute)
})
_communaute.subscribe((value) => {
communaute = value
initialised && updatePlot(puissance, communaute)
})
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', (event) => {
darkMode = !!event.matches
redraw()
})
.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'],
},
'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" ]
},
*/
}
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 }
data.profils.splice(i, 1)
data.profils = data.profils
await updatePlot(puissance, communaute)
let auto = 0
let totalProd = 0
let totalConso = 0
let soleil = {
hourly: {
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[]>[],
},
},
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,
}
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: {
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,
}
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) ||
pvgis.inputs.mounting_system.fixed.slope.value != inclinaison ||
pvgis.inputs.mounting_system.fixed.azimuth.value != azimuth
async function updatePv(
puissance: number,
communaute: (Grain.Communaute&{isMoving?: boolean}) | null,
await tick()
let annuelle = await fetch(`${base}/pvgis`, {
method: 'POST',
body: JSON.stringify({
lat: position?.lat,
lng: position?.lng,
azimuth: azimuth || 0,
inclinaison: inclinaison || 45,
}),
})
pvgis = await annuelle.json()
pvgis = await calcul.updatePv(puissance, communaute, pvgis, soleil, base)
}
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
}
}
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,
async function updatePlot(
puissance: number,
communaute: (Grain.Communaute & { isMoving?: boolean }) | null,
) {
console.log("isMoving", communaute?.isMoving)
if(communaute?.isMoving) {
return
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
}
}
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')
if (pr.length > 0) {
let pi = 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_[pi] = d
pi += 1
if (!browser) {
return
await tick()
redraw()
}
let has_plot = { has_plot: false }
let layout = {}
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;
await tick()
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')
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'
await tick()
redraw()
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,
},
]
let has_plot = { has_plot: false };
let layout = {};
var dataConso = [
{
values: [Math.round(auto), Math.round(totalConso - auto)],
labels: ['Autoproduit', 'Fournisseur'],
marker: { colors: ['#ffbf00dd', '#b81111dd'] },
type: 'pie',
sort: false,
hole: 0.5,
},
]
function relayout_(event: any) {
console.log('relayout_')
const plot = document.getElementById('plot')!
relayout(event, plot, Plotly, layout, has_plot, soleil, somme)
}
plotStyle = 'min-height:450px'
Plotly.react(
'autoProd',
dataProd,
{
title: "Autoconsommation sur l'année",
function redraw() {
console.log("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',
},
{ responsive: true }
)
Plotly.react(
'autoConso',
dataConso,
{
title: "Autoproduction sur l'année",
font: {
yaxis: {
title: { text: unite },
gridcolor: darkMode ? '#444' : '#bbb',
},
}
plotStyle = 'min-height:450px'
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,
},
]
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 }
)
}
{ 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 }
)
}
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 (kWh)</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>
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>
{#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"
<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>
{#if prmsFeature}
<h2 class="my-5">Relevés de compteurs</h2>
<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}"
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">
on:keydown={(e) => (e.key == 'ArrowUp' || e.key == 'ArrowDown') && e.preventDefault() }
type="number" />
</div>
<div class="col col-auto d-flex align-items-center">
class="btn btn-link"
on:click={(ev) => delPrm(ev, i)}
><i
class="bi bi-x-circle text-primary" /></button>
</td>
</tr>
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>
{#if data.accesParticuliers}
<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}
{#if (!data.accesParticuliers) || 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>
{: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}
<hr class="my-4" />
<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 data.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>
<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>
{#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" />
{/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}
{/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" />
<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>
<script lang="ts">
import { getContext } from 'svelte'
import type { Derived } from '../calcul'
const d: Derived = getContext("simulation")
const fmt2 = Intl.NumberFormat('fr-FR', { maximumFractionDigits: 2 })
const fmt0 = Intl.NumberFormat('fr-FR', { maximumFractionDigits: 0 })
function sum(x: number[], n: number) {
let sum = 0;
for(let i = 0; i < n; i++) {
sum += x[i]
}
return sum
}
function format(x: number) {
let f = 0
let s = ''
if (Math.abs(x) <= 1000) {
s = ' €'
f = x
} else if (Math.abs(x) < 1000000) {
s = ' k€'
f = x / 1000
} else if (Math.abs(x) < 1000000000) {
s = ' M€'
f = x / 1000000
} else {
s = ' mds€'
f = x / 1000000000
}
return fmt2.format(f) + s
}
const N = d.N
const resultatNet = d.resultatNet
const chiffreAffaires = d.chiffreAffaires
const arr = 0
const autoprod = d.p.autoprod
const autoconso = d.p.autoconso
const productionAnnuelle = d.productionAnnuelle
const tarifAllo = d.tarifAllo
const tarifLocal = d.tarifLocal
const tarifSurplus = d.tarifSurplus
const primeInvestissement = d.primeInvestissement
const charges = d.charges
const ebe = d.ebe
const valeurAjoutee = d.valeurAjoutee
const accise = d.accise
const taxeFonciere = d.taxeFonciere
const ifer = d.ifer
const cfe = d.cfe
const provisionOnduleurs = d.provisionOnduleurs
const interetsDetteSenior = d.interetsDetteSenior
const interetsDSRA = d.interetsDSRA
const resultatFiscal = d.resultatFiscal
const is = d.is
const investissement = d.investissement
const amortissement = d.amortissement
const apport = d.p.apport
const dscr = d.dscr
const exploitation = d.exploitation
const turpe = d.turpe
const assurance = d.assurance
const divers = d.divers
let an = new Date().getFullYear()
</script>
<div
class="tab-pane fade active show p-3"
id="compte-tab-pane"
role="tabpanel"
aria-labelledby="compte-tab"
tabindex="0">
<h3 class="my-3 fs-1 fw-bold">
<span class="text-primary">Projetez-vous</span> dans votre
projet.
</h3>
Durée d'analyse : {$N} ans
<h4 class="my-3 mt-5">Résultat</h4>
<div class="ms-3 row">
<h5 class="mt-5">Résultat net</h5>
<div class="fw-bold fs-4">
Cumulé : <span class="text-primary"
>{format(sum($resultatNet, $N))}</span>
</div>
<div class="d-flex flex-wrap">
<div class="d-flex flex-column my-3">
<div class="titreligne tcellt">Année</div>
<div class="titreligne tcellt">
Résultat net
</div>
</div>
{#each Array(Math.ceil($N)) as _, i}
<div class="d-flex flex-column my-3">
<div class="tcellt">{an + i}</div>
<div class="tcellt">
{$resultatNet[i].toFixed(arr)}
</div>
</div>
{/each}
</div>
</div>
<h4 class="my-3">Détail</h4>
<div class="ms-3">
<h5 class="mt-5">Chiffre d'affaires</h5>
<div class="fw-bold fs-4">
Cumulé : <span class="text-primary"
>{format(sum($chiffreAffaires, $N))}</span>
</div>
<div class="d-flex flex-wrap">
<div class="d-flex flex-column my-4">
<div class="titreligne tcellt">Année</div>
<div class="titreligne tcellt">
Autoproduction (€)
</div>
<div class="titreligne tcellt">
Vente locale (€)
</div>
<div class="titreligne tcellt">
Vente de surplus (€)
</div>
<div class="titreligne tcellt">Prime</div>
<div class="titreligne tcellt">
Chiffre d'affaires
</div>
</div>
{#each Array(Math.ceil($N)) as _, i}
<div class="d-flex flex-column my-4">
<div class="tcellt">{an + i}</div>
<div class="tcellt">
{(
$productionAnnuelle[i] *
(($autoprod * $tarifAllo[i]) / 100)
).toFixed(0)}
</div>
<div class="tcellt">
{(
$productionAnnuelle[i] *
(($autoconso * $tarifLocal[i]) / 100)
).toFixed(0)}
</div>
<div class="tcellt">
{(
$productionAnnuelle[i] *
((1 - $autoconso/100) * $tarifSurplus[i])
).toFixed(0)}
</div>
<div class="tcellt">{#if i == 0}{$primeInvestissement}{:else} {/if}</div>
<div class="tcellt">
{$chiffreAffaires[i].toFixed(arr)}
</div>
</div>
{/each}
</div>
<h5 class="my-3">Charges d'exploitation</h5>
<div class="fw-bold fs-4">
Cumulé : <span class="text-primary"
>{format(sum($charges, $N))}</span>
</div>
<div class="d-flex flex-wrap">
<div class="d-flex flex-column my-4">
<div class="titreligne tcellt">Année</div>
<div class="titreligne tcellt">
Maintenance (€)
</div>
<div class="titreligne tcellt">TURPE (€)</div>
<div class="titreligne tcellt">
Assurance (€)
</div>
<div class="titreligne tcellt">Divers</div>
<div class="titreligne tcellt">
Total charges
</div>
</div>
{#each Array(Math.ceil($N)) as _, i}
<div class="d-flex flex-column my-4">
<div class="tcellt">{an + i}</div>
<div class="tcellt">
{$exploitation[i].toFixed(arr)}
</div>
<div class="tcellt">
{$turpe[i].toFixed(arr)}
</div>
<div class="tcellt">
{$assurance[i].toFixed(arr)}
</div>
<div class="tcellt">
{$divers[i].toFixed(arr)}
</div>
<div class="tcellt">
{$charges[i].toFixed(arr)}
</div>
</div>
{/each}
</div>
<h5 class="my-3">Excédent brut d'exploitation</h5>
<div class="fw-bold fs-4">
Cumulé : <span class="text-primary"
>{format(sum($ebe, $N))}</span>
</div>
<div class="d-flex flex-wrap">
<div class="d-flex flex-column my-4">
<div class="titreligne tcellt">Année</div>
<div class="titreligne tcellt">
Valeur ajoutée (€)
</div>
<div class="titreligne tcellt">Accise (€)</div>
<div class="titreligne tcellt">IFER (€)</div>
<div class="titreligne tcellt">
Taxe foncière
</div>
<div class="titreligne tcellt">
Cotisation foncière des entreprises
</div>
<div class="titreligne tcellt">
Excédent brut d'exploitation
</div>
</div>
{#each Array(Math.ceil($N)) as _, i}
<div class="d-flex flex-column my-4">
<div class="tcellt">{an + i}</div>
<div class="tcellt">
{$valeurAjoutee[i].toFixed(arr)}
</div>
<div class="tcellt">
{$accise[i].toFixed(arr)}
</div>
<div class="tcellt">
{$ifer[i].toFixed(arr)}
</div>
<div class="tcellt">
{$taxeFonciere.toFixed(arr)}
</div>
<div class="tcellt">
{$cfe.toFixed(arr)}
</div>
<div class="tcellt">
{$ebe[i].toFixed(arr)}
</div>
</div>
{/each}
</div>
<h5 class="my-3">Résultat net</h5>
<div class="fw-bold fs-4">
Cumulé : <span class="text-primary"
>{format(sum($resultatNet, $N))}</span>
</div>
<div class="d-flex flex-wrap">
<div class="d-flex flex-column my-4">
<div class="titreligne tcellt">Année</div>
<div class="titreligne tcellt">Onduleurs
<button
type="button"
class="btn btn-sm btn-link align-baseline p-0 ms-2"
data-bs-toggle="popover"
data-bs-trigger="hover focus"
data-bs-content="Provision pour le remplacement des onduleurs"
><i class="bi bi-question-circle" /></button>
</div>
<div class="titreligne tcellt">
Amortissement
</div>
<div class="titreligne tcellt">
Intérêts dette senior
</div>
<div class="titreligne tcellt">
Intérêts DSRA <button
type="button"
class="btn btn-sm btn-link align-baseline p-0 ms-2"
data-bs-toggle="popover"
data-bs-trigger="hover focus"
data-bs-content="Dans un emprunt, compte bancaire qui permettra de payer le service de la dette si jamais l'entité emprunteuse n'est plus en mesure de le faire."
><i class="bi bi-question-circle" /></button>
</div>
<div class="titreligne tcellt">
Résultat fiscal
</div>
<div class="titreligne tcellt">
Impôt sur les sociétés
</div>
<div class="titreligne tcellt">
Résultat net
</div>
</div>
{#each Array(Math.ceil($N)) as _, i}
<div class="d-flex flex-column my-4">
<div class="tcellt">{an + i}</div>
<div class="tcellt">
{$provisionOnduleurs[i].toFixed(arr)}
</div>
<div class="tcellt">
{$amortissement[i].toFixed(arr)}
</div>
<div class="tcellt">
{$interetsDetteSenior[i].toFixed(arr)}
</div>
<div class="tcellt">
{$interetsDSRA[i].toFixed(arr)}
</div>
<div class="tcellt">
{$resultatFiscal[i].toFixed(arr)}
</div>
<div class="tcellt">
{$is[i].toFixed(arr)}
</div>
<div class="tcellt">
{$resultatNet[i].toFixed(arr)}
</div>
</div>
{/each}
</div>
{#if $investissement > $apport}
<h5 class="my-3">Couverture de la dette</h5>
<div class="d-flex flex-wrap">
<div class="d-flex flex-column my-3">
<div class="titreligne tcellt">Année</div>
<div class="titreligne tcellt">DSCR</div>
</div>
{#each Array(Math.ceil($N)) as _, i}
<div class="d-flex flex-column my-3">
<div class="tcellt">{an + i}</div>
<div class="tcellt">
{fmt2.format($dscr[i])}
</div>
</div>
{/each}
</div>
{/if}
</div>
</div>
<style lang="scss">
h5 {
font-size: 1.5rem;
font-weight: bold;
}
h4 {
font-size: 2rem;
font-weight: bold;
}
.tcellt {
border-bottom: 1px solid $primary;
}
.tcellt:last-child {
border-bottom: unset;
}
.tcellt {
padding: 8px 10px;
}
</style>
<script lang="ts">
import { Derived } from '../calcul'
import { getContext } from 'svelte'
const _simulation: Derived = getContext("simulation")
const inflationElec = _simulation.p.inflationElec
const enedis = _simulation.p.enedis
const productible = _simulation.p.productible
const ratio = _simulation.p.ratio
const degradation = _simulation.p.degradation
const tauxDSRA = _simulation.p.tauxDSRA
const inflation = _simulation.p.inflation
</script>
<div
class="tab-pane fade show active p-3"
id="expert-tab-pane"
role="tabpanel"
aria-labelledby="expert-tab"
tabindex="0">
<h3 class="my-3 fs-1 fw-bold">
D'avantage de <span class="text-primary">réglages</span>
</h3>
<div class="mt-5">
<div class="mb-3">
<label class="form-label" for="enedis"
>Coût de raccordement Enedis</label>
<input
class="form-control form-control-sm w-auto"
type="number"
id="enedis"
min="0"
bind:value={$enedis} />
</div>
<div class="mb-3">
<label class="form-label" for="productible"
>Productible annuel (kWh/kWc)</label>
<input
class="form-control form-control-sm w-auto"
type="number"
id="productible"
min="0"
bind:value={$productible} />
</div>
<div class="mb-3">
<label class="form-label" for="ratio"
>Ratio installation (€/W)</label>
<input
class="form-control form-control-sm w-auto"
type="number"
id="ratio"
min="0"
bind:value={$ratio} />
</div>
<div class="mb-3">
<label class="form-label" for="degradation"
>Taux de dégradation PV(%)</label>
<input
class="form-control form-control-sm w-auto"
type="number"
id="degradation"
min="0"
bind:value={$degradation} />
</div>
<div class="mb-3">
<label class="form-label" for="dsra"
>Taux d'intérêt DSRA (%)</label>
<input
class="form-control form-control-sm w-auto"
type="number"
id="dsra"
min="0"
step="any"
bind:value={$tauxDSRA} />
</div>
<div class="mb-3">
<label class="form-label" for="inflation"
>Inflation tarif local</label>
<input
class="form-control form-control-sm w-auto"
type="number"
id="inflation"
min="0"
bind:value={$inflation} />
</div>
<div class="mb-3">
<label class="form-label" for="inflationElec"
>Inflation nationale de l'électricité (%)</label>
<input
class="form-control form-control-sm w-auto"
type="number"
bind:value={$inflationElec}
min="0"
id="inflationElec" />
</div>
<!-- <div class="my-3">
<label class="form-label" for="surfacekwc">Surface d'un kWc (m²)</label>
<input
class="form-control form-control-sm"
type="number"
id="surfacekwc"
step ="any"
value={surface_kwc}
on:change={onRatioChange} />
</div>-->
</div>
</div>
export let position: undefined | { lat: number; lng: number }
export let azimuth: undefined | number
export let inclinaison: undefined | number
import type { Grain } from '../../../../grain-types.ts';
import type { Writable } from 'svelte/store'
import { getContext } from 'svelte'
type Compteur_ = Grain.Compteur & { dot?: any };
type Communaute_ = Grain.Communaute & { compteurs?: Compteur_[], circle?: any, isMoving?: boolean };
let communaute: Writable<Communaute_ | null> = getContext("communaute")
export let save: () => Promise<void> = async () => { };
let derogation = $communaute?.derogation ?? "non"
let circleOpts = {
radius: 1000,
stroke: true,
fillOpacity: 0,
color: '#b81413',
weight: 3,
}
let leaflet: any = null;
function changeDerogation (e: any) {
console.log('Changederogation', derogation, $communaute, e)
if($communaute) {
$communaute.derogation = derogation;
let d = derogationRayonKm() * 1000
checkRayon($communaute, d)
if ($communaute?.circle) {
$communaute.circle.setRadius(d)
} else {
circleOpts.radius = d
$communaute.circle = leaflet.circle($communaute, circleOpts)
$communaute.circle.addTo(map!)
}
save()
}
}
function derogationRayonKm(): number {
if ($communaute?.derogation == "20") {
return 10
} else if ($communaute?.derogation == "10") {
return 5
} else {
return 1
}
}
type Commune = {
nom: String,
densite: number,
}
let communes: null | Commune[] = null
async function checkRayon(latlng: Grain.LatLng| null, rayon: number) {
if(latlng) {
console.log("checkRayon");
let resp = await fetch(`${srv}/api/intersect?lat=${latlng.lat}&lng=${latlng.lng}&rayon=${rayon}`, {
credentials: 'include',
})
if (resp.status == 200) {
communes = await resp.json()
}
}
}
iconUrl: `${assets}/marker-icon-red.png`,
iconRetinaUrl: `${assets}/marker-icon-red.png`,
shadowUrl: `${assets}/marker-shadow.png`,
iconUrl: `${assets}/signal-red.png`,
iconRetinaUrl: `${assets}/signal-red.png`,
})
let initialCentre: null | { lat: number; lng: number } = null
let invalidDrag = false
map.on('mousemove', function (e: any) {
if (invalidDrag) {
return
}
if ($communaute?.circle && e.originalEvent.shiftKey) {
let m = document.getElementById('descrmap')
m!.focus()
let d = derogationRayonKm() * 1000
if (initialCentre) {
let testCentre = {
lat: e.latlng.lat - initialCentre.lat,
lng: e.latlng.lng - initialCentre.lng,
}
if (!checkDistance(testCentre, $communaute.compteurs ?? [], d)) {
invalidDrag = true
return
}
$communaute.lat = testCentre.lat
$communaute.lng = testCentre.lng
$communaute.circle.setLatLng($communaute);
} else {
if (map.distance($communaute, e.latlng) <= d) {
$communaute.isMoving = true;
initialCentre = {
lat: e.latlng.lat - $communaute.lat,
lng: e.latlng.lng - $communaute.lng,
}
}
}
}
position = e.latlng
console.log(position)
if (dot) {
dot.setLatLng(position!)
console.log(e);
if(e.originalEvent.shiftKey) {
// Déplacer/ajouter la communauté
if($communaute?.circle) {
if(checkDistance(e.latlng, $communaute.compteurs ?? [], derogationRayonKm() * 1000)) {
$communaute.circle.setLatLng(e.latlng)
$communaute.lat = e.latlng.lat
$communaute.lng = e.latlng.lng
}
} else {
if(!$communaute) {
$communaute = { lat: e.latlng.lat, lng: e.latlng.lng, derogation: derogation }
}
circleOpts.radius = derogationRayonKm() * 1000;
$communaute!.circle = L.circle(
e.latlng,
circleOpts
)
$communaute!.circle.addTo(map)
}
checkRayon($communaute!, derogationRayonKm() * 1000)
save()
dot = L.marker(position!, {
icon: redIcon!,
draggable: false,
})
dot.addTo(map)
if(!$communaute) {
communaute.set({
lat: e.latlng.lat,
lng: e.latlng.lng,
compteurs: [],
derogation,
})
circleOpts.radius = derogationRayonKm() * 1000;
$communaute!.circle = L.circle(
e.latlng,
circleOpts
).addTo(map)
checkRayon($communaute, derogationRayonKm() * 1000)
}
if(map.distance($communaute!, e.latlng) <= derogationRayonKm() * 1000) {
let dot = {
lat: e.latlng.lat,
lng: e.latlng.lng,
dot: L.marker(e.latlng, {
icon: redIcon!,
draggable: false,
}),
azimuth: 0,
inclinaison: 0,
puissance: 0,
};
dot.dot.addTo(map)
if(!$communaute!.compteurs) {
$communaute!.compteurs = []
}
$communaute!.compteurs.push(dot);
$communaute = $communaute;
}
save()
map.setView(position, zoom)
let lng0 = 180
let lng1 = -180
let lat0 = 90
let lat1 = -90
let mar = 0.5
let dy = (mar + (derogationRayonKm())) / 111
let lat2 = $communaute.lat - dy
let lat3 = $communaute.lat + dy
let dx = dy / Math.cos(($communaute.lat * Math.PI) / 180)
let lng2 = $communaute.lng - dx
let lng3 = $communaute.lng + dx
lng0 = Math.min(lng0, lng2)
lng1 = Math.max(lng1, lng3)
lat0 = Math.min(lat0, lat2)
lat1 = Math.max(lat1, lat3)
if (lng0 < lng1 && lat0 < lat1) {
console.log([
[lat0, lng0],
[lat1, lng1],
])
map.fitBounds(
[
[lat0, lng0],
[lat1, lng1],
],
)
} else {
map.setView($communaute, zoom)
}
}
function checkDistance(
e: { lat: number; lng: number },
compteurs: { lat: number, lng: number }[],
d: number
): boolean {
for (let v of compteurs) {
let dist = map.distance(e, v)
if (dist > d) {
return false
}
}
return true
}
function deleteCompteur(
event: any,
i: number,
) {
event.preventDefault();
$communaute!.compteurs![i].dot.removeFrom(map);
$communaute!.compteurs!.splice(i, 1);
$communaute = $communaute;
save()
<div id="descrmap" class="w-100" style="height:500px" />
<div class="d-flex flex-column">
<button
class="btn btn-sm btn-outline-secondary my-2 ms-auto"
on:click={(_) => recentrer(zoom)}>Recentrer</button>
<div
class="tab-pane fade active show p-3"
id="description-tab-pane"
role="tabpanel"
aria-labelledby="description-tab"
tabindex="0">
<h3 class="my-3 fs-1 fw-bold">
Quelques <span class="text-primary">détails</span> de plus.
</h3>
<label class="form-label" for="azimuth"
>Azimuth (degrés par rapport au Nord):</label>
<input
type="number"
class="form-control"
id="azimuth"
bind:value={azimuth} />
Cliquez sur la carte ci-dessous pour ajouter des compteurs, ou cliquez en maintenant la touche Shift pour positionner la communauté.
<div class="my-3">
<label class="form-label" for="incl">Inclinaison:</label>
<input
type="number"
class="form-control"
id="incl"
bind:value={inclinaison} />
<div id="descrmap" class="w-100" style="height:500px" />
<div class="d-flex flex-column">
<button
class="btn btn-sm btn-outline-secondary my-2 ms-auto"
on:click={(_) => recentrer(zoom)}>Recentrer</button>
{#if $communaute?.compteurs?.length}
<div class="my-3">
<table class="table">
<thead>
<tr><th>Puissance</th><th>Azimuth (le Sud est à 0°, l'Est à -90°, l'Ouest à 90°)</th><th>Inclinaison</th><th></th></tr>
</thead>
<tbody>
{#each $communaute.compteurs as c, i}
<tr>
<td>
<input
type="number"
class="form-control"
bind:value={c.puissance} />
</td>
<td>
<input
type="number"
class="form-control"
bind:value={c.azimuth} />
</td>
<td>
<input
type="number"
class="form-control"
bind:value={c.inclinaison} />
</td>
<td>
<button class="btn btn-sm btn-outline-primary" on:click={(e) => deleteCompteur(e, i)}><i class="bi bi-trash"/></button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
<div class="p-3 overflow-auto">
<label for="derog" class="form-label">
Extension de périmètre (habitat rural et/ou dispersé)
</label> <button type="button"
class="btn btn-link align-baseline p-0"
id="ext-popover"
data-bs-toggle="popover"
data-bs-trigger="hover focus"
data-bs-content="Par dérogation, le rayon de la communauté peut être étendu à 5km ou 10km.">
<i class="bi bi-question-circle" />
</button>
<select
id="derog"
class="form-select form-select-sm w-auto d-inline"
name="derog"
bind:value={derogation}
on:change={changeDerogation}>
<option value="non">Non</option>
<option value="10">10km</option>
<option value="20">20km</option>
</select>
{#if communes && communes.length}
<h4 class="mt-5">Communes concernées</h4>
<table class="table">
<thead>
<th>Nom</th><th>Éligibilité</th>
</thead>
<tbody>
{#each communes as c}
<tr><td>{c.nom}</td><td>{#if c.densite >= 5}20km{:else if c.densite >= 3}10km{#if derogation=='20' }<i class="ms-3 text-danger bi bi-exclamation-triangle"/>{/if}{:else}2km{#if derogation!='non' }<i class="ms-3 text-danger bi bi-exclamation-triangle"/>{/if}{/if}</td></tr>
{/each}
</tbody>
</table>
{/if}
</div>
import { readable, writable, derived } from 'svelte/store';
import type { Readable, Writable } from 'svelte/store';
import type { Grain } from '../../../helpers'
import * as H from '../../../helpers'
import type { Sample } from '../../../plot'
import { sample } from '../../../plot'
import { tick } from 'svelte'
type _SimulationCommunaute = {
[Property in keyof Grain.SimulationCommunaute]: Writable<Grain.SimulationCommunaute[Property]>;
}
export class Derived {
p: _SimulationCommunaute;
s: Grain.SimulationCommunaute
constructor(s: Grain.SimulationCommunaute) {
this.s = s
this.p = <_SimulationCommunaute>Object.fromEntries(
Object.entries(s).map(([k, v], _) => {
let w = writable(v);
w.subscribe((value) => this.s[k as keyof Grain.SimulationCommunaute] = value as any)
return [k, w]
})
);
this.N = writable(30);
this.investissement = derived(
[this.p.puissance, this.p.ratio, this.p.enedis],
([$puissance, $ratio, $enedis]) => $puissance ? $puissance * 1000 * $ratio + $enedis : 0)
this.apport = derived(
[this.investissement, this.p.apport],
([$investissement, $apport]) => Math.min($investissement, $apport)
)
this.primeInvestissement = derived(
this.p.puissance,
($puissance) => ($puissance <= 3 ? 0.30
: $puissance <= 9 ? 0.23
: $puissance <= 36 ? 0.20
: $puissance <= 100 ? 0.10
: 0) * 1000 * $puissance
)
function tarifSurplus_(tarifCRE: any, puissance: number): number {
let resultat = tarifCRE.prix[0]
for (let i = 0; i < tarifCRE.limites.length; i++) {
if (puissance < tarifCRE.limites[i]) {
return resultat
} else {
resultat = tarifCRE.prix[i]
}
}
return resultat
}
this.tarifSurplus = derived(
[this.p.tarifCRE, this.p.puissance, this.N],
([$tarifCRE, $puissance, $N],
_,
update
) => {
update((tarifSurplus) => {
if (!tarifSurplus || tarifSurplus.length < $N) {
tarifSurplus = Array($N);
}
let t = tarifSurplus_($tarifCRE, $puissance);
for (let i = 0; i < $N; i++) {
tarifSurplus[i] = t * Math.pow(1 + $tarifCRE.inflation / 100, i)
}
return tarifSurplus
})
}
);
this.productionAnnuelle = derived(
[this.p.productible, this.p.puissance, this.p.degradation, this.N],
([$productible, $puissance, $degradation, $N], _, update) => {
update((prod) => {
if (!prod || prod.length < $N) {
prod = Array($N);
}
for (let i = 0; i < $N; i++) {
prod[i] = H.productionAnnuelle($productible, $puissance, $degradation, i)
}
return prod
})
}
)
this.tarifLocal = derived(
[this.p.prix, this.p.inflation, this.N],
([$prix, $inflation, $N], _, update) => {
update((tarif) => {
if (!tarif || tarif.length < $N) {
tarif = Array($N);
}
for (let i = 0; i < $N; i++) {
tarif[i] = $prix * Math.pow(1 + $inflation / 100, i)
}
return tarif
})
}
)
this.tarifAllo = derived(
[this.p.coutElec, this.p.inflationElec, this.N],
([$coutElec, $inflationElec, $N], _, update) => {
update((tarif) => {
if (!tarif || tarif.length < $N) {
tarif = Array($N);
}
for (let i = 0; i < $N; i++) {
tarif[i] = $coutElec * Math.pow(1 + $inflationElec / 100, i)
}
return tarif
})
}
)
this.chiffreAffaires = derived(
[this.primeInvestissement, this.productionAnnuelle,
this.p.autoconso, this.tarifLocal, this.tarifSurplus,
this.p.autoprod, this.tarifAllo, this.N],
([$primeInvestissement, $productionAnnuelle, $autoconso, $tarifLocal, $tarifSurplus, $autoprod, $tarifAllo, $N], _, update) => {
update((ca) => {
if (!ca || ca.length < $N) {
ca = Array($N);
}
for (let i = 0; i < $N; i++) {
ca[i] = (i == 0 ? $primeInvestissement : 0)
+ $productionAnnuelle[i] * (
$autoconso / 100 * $tarifLocal[i] +
(1 - $autoconso / 100) * $tarifSurplus[i] +
($autoprod / 100 * $tarifAllo[i])
)
}
return ca
})
}
)
this.turpe = derived(
[this.p.puissance, this.p.turpe, this.p.autoconso, this.productionAnnuelle, this.p.turpeInjection, this.p.inflation, this.N],
([$puissance, $turpe, $autoconso, $productionAnnuelle, $turpeInjection, $inflation, $N], _, update) => {
update((turpe) => {
if (!turpe || turpe.length < $N) {
turpe = Array($N);
}
for (let i = 0; i < $N; i++) {
turpe[i] = (
$puissance * $turpe +
(1 - $autoconso / 100) * $productionAnnuelle[i] * $turpeInjection
) * Math.pow(1 + $inflation / 100, i)
}
return turpe
})
}
)
this.assurance = derived(
[this.investissement, this.p.assurance, this.p.inflation, this.N],
([$investissement, $assurance, $inflation, $N], _, update) => {
update((assurance) => {
if (!assurance || assurance.length < $N) {
assurance = Array($N);
}
for (let i = 0; i < $N; i++) {
assurance[i] = (($investissement * $assurance) / 100)
* Math.pow(1 + $inflation / 100, i)
}
return assurance
})
}
)
this.exploitation = derived(
[this.p.puissance, this.p.maintenance, this.p.inflation, this.N],
([$puissance, $maintenance, $inflation, $N], _, update) => {
update((expl) => {
if (!expl || expl.length < $N) {
expl = Array($N);
}
for (let i = 0; i < $N; i++) {
expl[i] = $puissance * $maintenance
* Math.pow(1 + $inflation / 100, i)
}
return expl
})
}
)
this.divers = derived(
[this.p.divers, this.p.puissance, this.p.inflation, this.N],
([$divers, $puissance, $inflation, $N], _, update) => {
update((divers) => {
if (!divers || divers.length < $N) {
divers = Array($N);
}
for (let i = 0; i < $N; i++) {
divers[i] = $divers * $puissance
* Math.pow(1 + $inflation / 100, i)
}
return divers
})
}
)
this.charges = derived(
[this.investissement, this.p.fraisDivers, this.exploitation, this.turpe, this.assurance, this.divers, this.N],
([$investissement, $fraisDivers, $exploitation, $turpe, $assurance, $divers, $N], _, update) => {
update((charges) => {
if (!charges || charges.length < $N) {
charges = Array($N);
}
for (let i = 0; i < $N; i++) {
charges[i] = ($investissement && i == 0 ? $fraisDivers : 0) + $exploitation[i] + $turpe[i] + $assurance[i] + $divers[i]
}
return charges
})
}
)
this.valeurAjoutee = derived(
[this.chiffreAffaires, this.charges, this.N],
([$chiffreAffaires, $charges, $N], _, update) => {
update((va) => {
if (!va || va.length < $N) {
va = Array($N);
}
for (let i = 0; i < $N; i++) {
va[i] = $chiffreAffaires[i] - $charges[i]
}
return va
})
}
)
this.amortissement = derived(
[this.p.dureeAmortissement, this.investissement, this.N],
([$dureeAmortissement, $investissement, $N], _, update) => {
update((a) => {
if (!a || a.length < $N) {
a = Array($N);
}
for (let i = 0; i < $N; i++) {
if (i < $dureeAmortissement) {
a[i] = $investissement / $dureeAmortissement
} else {
a[i] = 0
}
}
return a
})
}
);
this.provisionOnduleurs = derived(
[this.p.puissance, this.p.provisionOnduleurs, this.p.inflation, this.N],
([$puissance, $provision, $inflation, $N], _, update) => {
update((a) => {
if (!a || a.length < $N) {
a = Array($N);
}
for (let i = 0; i < $N; i++) {
a[i] = $puissance * 1000 * $provision * Math.pow(1 + $inflation / 100, i) / 10
}
return a
})
}
);
// Accise: taux dérogatoire d'1€/MWh jusqu'au 31/1/2025
this.accise = derived(
[this.productionAnnuelle, this.N],
([$productionAnnuelle, $N], _, update) => {
update((a) => {
if (!a || a.length < $N) {
a = Array($N);
}
for (let i = 0; i < $N; i++) {
a[i] = $productionAnnuelle[i] * 0.001
}
return a
})
}
);
// Exonération de taxe foncière: https://www.legifrance.gouv.fr/codes/article_lc/LEGIARTI000044996150
this.taxeFonciere = readable(0)
// Formule compliquée, exonéré sur les cas concrets
this.cfe = readable(0)
this.ifer = derived(
[this.p.puissance, this.p.ratioDcAc, this.N],
([$puissance, $ratio, $N], _, update) => {
update((a) => {
if (!a || a.length < $N) {
a = Array($N);
}
for (let i = 0; i < $N; i++) {
a[i] = $puissance < 100 ? 0 : 3.394 * $puissance * $ratio
}
return a
})
}
);
this.impots = derived(
[this.ifer, this.taxeFonciere, this.cfe, this.N],
([$ifer, $taxeFonciere, $cfe, $N], _, update) => {
update((a) => {
if (!a || a.length < $N) {
a = Array($N);
}
for (let i = 0; i < $N; i++) {
a[i] = $ifer[i] + $taxeFonciere + $cfe
}
return a
})
}
);
this.ebe = derived(
[this.valeurAjoutee, this.impots, this.N],
([$va, $impots, $N], _, update) => {
update((a) => {
if (!a || a.length < $N) {
a = Array($N);
}
for (let i = 0; i < $N; i++) {
a[i] = $va[i] - $impots[i]
}
return a
})
}
);
this.dette = derived(
[this.investissement, this.p.apport],
([$inv, $app]) => $inv - $app
)
this.dette.subscribe(this.banque.dette.set)
this.interetsDetteSenior = derived(
[this.banque.resultat, this.N],
([$banque, $N], _, update) => {
update((ids) => {
if (!ids || ids.length < $N) {
ids = Array($N);
}
for (let i = 0; i < $N; i++) {
ids[i] = i < $banque.interetsAnnuels.length ? $banque.interetsAnnuels[i] : 0
}
return ids
})
}
)
this.interetsDSRA = derived(
[this.interetsDetteSenior, this.p.tauxDSRA, this.N],
([$ids, $taux, $N], _, update) => {
update((a) => {
if (!a || a.length < $N) {
a = Array($N);
}
for (let i = 0; i < $N; i++) {
a[i] = $ids[i] * $taux / 100
}
return a
})
}
);
this.resultatFiscal = derived(
[this.ebe, this.amortissement, this.provisionOnduleurs, this.interetsDetteSenior, this.interetsDSRA, this.N],
([$ebe, $amortissement, $provision, $ids, $dsra, $N], _, update) => {
update((rf) => {
if (!rf || rf.length < $N) {
rf = Array($N);
}
for (let i = 0; i < $N; i++) {
rf[i] = $ebe[i] - $amortissement[i] - $provision[i] - $ids[i] - $dsra[i]
}
return rf
})
}
)
this.is = derived(
[this.resultatFiscal, this.p.tauxIS, this.N],
([$r, $taux, $N], _, update) => {
update((is) => {
if (!is || is.length < $N) {
is = Array($N);
}
for (let i = 0; i < $N; i++) {
is[i] = $r[i] <= 0 ? 0 : ($r[i] * $taux / 100)
}
return is
})
}
)
this.resultatNet = derived(
[this.resultatFiscal, this.is, this.N],
([$r, $is, $N], _, update) => {
update((r) => {
if (!r || r.length < $N) {
r = Array($N);
}
for (let i = 0; i < $N; i++) {
r[i] = $r[i] - $is[i]
}
return r
})
}
)
this.tresorerieDisponibleDetteSenior = derived(
[this.ebe, this.is, this.N],
([$ebe, $is, $N], _, update) => {
update((r) => {
if (!r || r.length < $N) {
r = Array($N);
}
for (let i = 0; i < $N; i++) {
r[i] = $ebe[i] - $is[i]
}
return r
})
}
)
this.serviceDetteSenior = derived(
[this.interetsDSRA, this.interetsDetteSenior, this.banque.resultat, this.N],
([$dsra, $ids, $b, $N], _, update) => {
update((r) => {
if (!r || r.length < $N) {
r = Array($N);
}
for (let i = 0; i < $N; i++) {
r[i] = $dsra[i] + $ids[i] + $b.remboursementAnnuel[i]
}
return r
})
}
)
this.dscr = derived(
[this.banque.resultat, this.tresorerieDisponibleDetteSenior,
this.serviceDetteSenior, this.N],
([$b, $treso, $service, $N], _, update) => {
update((r) => {
if (!r || r.length < $N) {
r = Array($N);
}
for (let i = 0; i < $N; i++) {
if (i < $b.remboursementAnnuel.length) {
r[i] = $treso[i] / $service[i]
} else {
r[i] = 0
}
}
return r
})
}
)
}
investissement: Readable<number>;
apport: Readable<number>;
primeInvestissement: Readable<number>;
N: Writable<number>;
tarifSurplus: Readable<Array<number>>;
productionAnnuelle: Readable<Array<number>>;
tarifLocal: Readable<Array<number>>;
tarifAllo: Readable<Array<number>>;
chiffreAffaires: Readable<Array<number>>;
turpe: Readable<Array<number>>;
assurance: Readable<Array<number>>;
exploitation: Readable<Array<number>>;
divers: Readable<Array<number>>;
charges: Readable<Array<number>>;
valeurAjoutee: Readable<Array<number>>;
accise: Readable<Array<number>>;
taxeFonciere: Readable<number>;
cfe: Readable<number>;
ifer: Readable<Array<number>>;
impots: Readable<Array<number>>;
ebe: Readable<Array<number>>;
amortissement: Readable<Array<number>>;
resultatFiscal: Readable<Array<number>>;
provisionOnduleurs: Readable<Array<number>>;
interetsDetteSenior: Readable<Array<number>>;
interetsDSRA: Readable<Array<number>>;
is: Readable<Array<number>>;
dette: Readable<number>;
resultatNet: Readable<Array<number>>;
dscr: Readable<Array<number>>;
tresorerieDisponibleDetteSenior: Readable<Array<number>>;
serviceDetteSenior: Readable<Array<number>>;
banque: Banque = new Banque();
}
import { ppmt } from 'financial'
export type Resultat = {
remboursement: Array<number>,
remboursementAnnuel: number[],
crd: number[],
crdAnnuel: number[],
interets: number[],
interetsAnnuels: number[]
}
export class Banque {
// V3
dette = writable(0);
// V4
taux = writable(0);
// Z8
duree = writable(0);
// Z7
dureeDifferee = writable(1)
// V8
nombrePeriodesParAn = writable(12)
prorataPeriodique = derived(this.nombrePeriodesParAn, (n) => 1 / n)
// V6
dureeAns = derived([this.duree, this.nombrePeriodesParAn], ([$a, $b]) => $a / $b);
dureeAns_ = derived(this.dureeAns, Math.ceil)
taux_ = derived(this.taux, (t) => Math.min(t, 1))
// V10
nombreEcheancesPeriodeTronquee = derived([this.dureeAns, this.nombrePeriodesParAn], ([a, b]) => a * b);
// V9
nombreEcheances = derived(this.nombreEcheancesPeriodeTronquee, Math.ceil)
// W11
totalPeriodes = derived([this.dureeAns, this.nombrePeriodesParAn, this.dureeDifferee], ([$a, $b, $c]) => $a * $b - $c)
// X11
periodeRemboursementCapital = this.dureeDifferee
resultat: Readable<Resultat> = derived([
this.duree, this.dureeAns_, this.dette,
this.periodeRemboursementCapital,
this.nombreEcheancesPeriodeTronquee,
this.taux_,
this.prorataPeriodique,
this.periodeRemboursementCapital,
this.totalPeriodes,
this.nombreEcheances,
this.nombrePeriodesParAn,
], ([$duree, $dureeAns_, $dette, $periode,
$nombreEcheancesPeriodeTronquee,
$taux_,
$prorataPeriodique,
$periodeRemboursementCapital,
$totalPeriodes,
$nombreEcheances,
$nombrePeriodesParAn,
], _, update) => {
update((resultat) => {
if (resultat.remboursement.length != $duree + 1) {
resultat.remboursement = new Array($duree + 1)
resultat.crd = new Array($duree + 1)
resultat.interets = new Array($duree + 1)
resultat.remboursementAnnuel = new Array($dureeAns_)
resultat.crdAnnuel = new Array($dureeAns_)
resultat.interetsAnnuels = new Array($dureeAns_)
}
resultat.crd[0] = $dette
resultat.remboursement[0] = 0
resultat.interets[0] = 0
resultat.crdAnnuel[0] = 0
resultat.remboursementAnnuel[0] = 0
resultat.interetsAnnuels[0] = 0
for (let i = 1; i <= $duree; i++) {
if (i <= $periode) {
resultat.remboursement[i] = 0
} else if (i < $nombreEcheancesPeriodeTronquee) {
resultat.remboursement[i] = ppmt(
$taux_ * $prorataPeriodique,
i - $periodeRemboursementCapital,
$totalPeriodes,
-$dette
)
} else if (i == $nombreEcheances) {
resultat.remboursement[i] = resultat.crd[i - 1]
} else {
resultat.remboursement[i] = 0
}
resultat.crd[i] = resultat.crd[i - 1] - resultat.remboursement[i]
resultat.interets[i] = resultat.crd[i - 1] * $taux_ * $prorataPeriodique
if (i % $nombrePeriodesParAn == 0) {
resultat.crdAnnuel[i / $nombrePeriodesParAn] = resultat.crd[i]
resultat.remboursementAnnuel[i / $nombrePeriodesParAn] = 0
resultat.interetsAnnuels[i / $nombrePeriodesParAn] = 0
}
resultat.remboursementAnnuel[Math.floor(i / $nombrePeriodesParAn)] +=
resultat.remboursement[i]
resultat.interetsAnnuels[Math.floor(i / $nombrePeriodesParAn)] += resultat.interets[i]
}
return resultat
})
}, {
remboursement: new Array(0),
remboursementAnnuel: new Array(0),
crd: new Array(0),
crdAnnuel: new Array(0),
interets: new Array(0),
interetsAnnuels: new Array(0),
});
}
export type PVGIS = {
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
}[],
},
}
export async function updatePv(
puissance: number,
communaute: (Grain.Communaute & { isMoving?: boolean }) | null,
pvgis: PVGIS | null,
soleil: Sample,
base: string,
): Promise<PVGIS|null> {
if (communaute?.isMoving) {
return pvgis
}
console.log("updatePv", puissance);
let azimuth = 0;
let p_total = 0;
let inclinaison = 0;
let lat = Math.round(communaute?.lat || 45.56747);
let lng = Math.round(communaute?.lng || 5.92285);
console.log("com", communaute)
if (communaute?.compteurs) {
for (let c of communaute.compteurs) {
azimuth += c.azimuth * c.puissance;
inclinaison += c.inclinaison * c.puissance;
p_total += c.puissance;
}
if (p_total <= puissance) {
inclinaison += lat * (puissance - p_total)
} else {
azimuth *= puissance / p_total;
inclinaison *= puissance / p_total;
}
if (puissance > 0) {
azimuth /= puissance
inclinaison /= puissance
} else {
azimuth = 0;
inclinaison = lat;
}
azimuth = Math.round(azimuth)
inclinaison = Math.round(inclinaison)
}
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 ||
pvgis.inputs.location.latitude != lat ||
pvgis.inputs.location.longitude != lng ||
pvgis.inputs.mounting_system.fixed.slope['value'] != inclinaison ||
pvgis.inputs.mounting_system.fixed.azimuth['value'] != azimuth
) {
await tick()
console.log(inclinaison, azimuth, p_total);
let annuelle = await fetch(`${base}/pvgis?lat=${lat}&lng=${lng}&azimuth=${azimuth}&inclinaison=${inclinaison}`)
pvgis = await annuelle.json()
}
console.log(pvgis);
soleil.hourly = { x: [], y: [] };
soleil.daily = { x: [], y: [] };
soleil.weekly = { x: [], y: [] };
const start = new Date(min_t * 1000)
const janvier = new Date(start.getFullYear(), 0, 1)
console.log("UP")
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) {
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
}
}
}
return pvgis
}
type Fetch = (input: string) => Promise<Response>;
export async function profile_(assets: string, profils_: Record<string, { profils: string[] }>, name: string, _fetch?: Fetch): Promise<{ coefs: number[], total: number }> {
await tick();
console.log("assets", assets)
let profiles = await Promise.all(
profils_[name].profils.map(async (x) => {
let url = `${assets}/profiles/${x}.csv`
console.log("url", url)
let resp = await (_fetch? _fetch:fetch)(url)
return await resp.text()
})
)
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 }
}
export let profil_cache: Map<string, { coefs: number[]; total: number }> = new Map()
export async function updatePlot(
assets: string,
annee: Record<number, number[]>,
profils: Grain.Profil[],
profils_: Record<string, { profils: string[] }>,
somme: Sample,
soleil: Sample,
fetch?: Fetch,
): Promise<{auto: number, totalProd: number, totalConso: number}> {
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 (profils.length > 0) {
let pi = 0;
for (let p of profils) {
console.log("profile", p);
let d: { t: number[]; y: number[]; i: number } = {
t: [],
y: [],
i: 0,
}
let i = 0
let pp = profil_cache.get(p.type)
if (!pp) {
profil_cache.set(p.type, await profile_(assets, profils_, p.type, fetch))
}
console.log("pp", pp);
for (let t = min_t; t <= max_t; t += 1800) {
d.t.push(t)
if (pp) {
d.y.push((pp.coefs[i] * p.conso) / pp.total)
}
i += 1
}
annee_[pi] = d
pi += 1
}
}
return sample(min_t, max_t, annee_, somme, soleil, {
formatMatcher: "month, year",
})
}
<script lang="ts">
import type { Banque } from '../calcul'
import { getContext } from 'svelte'
const b: Banque = getContext("banque")
let annuel = true
const banque = b.resultat
</script>
<div
class="tab-pane fade active show p-3"
id="banque-tab-pane"
role="tabpanel"
aria-labelledby="banque-tab"
tabindex="0">
<h3 class="my-3 fs-1 fw-bold">
Tous <span class="text-primary">les recoins</span> de votre
emprunt.
</h3>
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="annuel"
bind:checked={annuel} />
<label class="form-check-label" for="annuel">Présentation annuelle</label>
</div>
{#if annuel}
<div class="my-3">
<table class="table table-striped border-0 d-inline-block banque">
<thead>
<tr>
<th>Année</th>
<th>Capital restant dû</th>
<th>Principal</th>
<th>Intérêts</th>
</tr>
</thead>
<tbody>
{#each $banque.remboursementAnnuel as r, periode}
<tr>
<td>{periode}</td>
<td>{$banque.crdAnnuel[periode].toFixed(0)}</td>
<td>{r.toFixed(0)}</td>
<td>{$banque.interetsAnnuels[periode].toFixed(0)}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{:else}
<div class="my-3">
<table class="table table-striped border-0 d-inline-block">
<thead>
<tr>
<th>Mois</th>
<th>Capital restant dû</th>
<th>Principal</th>
<th>Intérêts</th>
</tr>
</thead>
<tbody>
{#each $banque.remboursement as r, periode}
<tr>
<td>{periode}</td>
<td>{$banque.crd[periode].toFixed(0)}</td>
<td>{r.toFixed(0)}</td>
<td
>{#if periode > 0}
{$banque.interets[periode].toFixed(0)}
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
<style lang="scss">
table {
font-size: 90%;
}
th {
font-size: 1rem;
font-weight: bold;
}
td {
padding: 5px 5px;
padding: 5px 7px 5px 10px;
text-align: end;
border: none;
}
</style>
import Banque from '../../Banque.svelte'
import Simulation from './Simulation.svelte'
import Description from './Description.svelte'
import { page } from '$app/stores'
import { assets as a } from '$app/paths'
import { enhance, applyAction } from '$app/forms'
import { onMount } from 'svelte'
import type { SubmitFunction } from '@sveltejs/kit'
import type { Grain } from '../../../helpers'
import type { Modal, Offcanvas } from 'bootstrap'
import { email } from '../../../stores'
export let prmsFeature: boolean = false
export let assets: string = a
export let server = 'https://coturnix.fr'
let zoom = 5
type Prm = { prm: number | null; nom: string; prenom: string }
export let data: {
nom: string
porteur: string
adresse: string
tel: string
id: string
centrales: Grain.Centrale[]
img?: string
simulation: Grain.SimulationCommunaute
guest?: boolean
prms: Prm[]
email?: string
}
import { Derived } from './calcul'
import { getContext } from 'svelte'
$: p = data.simulation
onMount(async () => {
email.set(data.email || null)
email.subscribe((value) => {
if (value) data.email = value
})
const tarifLocal = _simulation.tarifLocal
const chiffreAffaires = _simulation.chiffreAffaires
const N = _simulation.N
const investissement = _simulation.investissement
const charges = _simulation.charges
const interetsDetteSenior = _simulation.interetsDetteSenior
const banque = _simulation.banque.resultat
const resultatNet = _simulation.resultatNet
const _coutElec = _simulation.p.coutElec
const inflationElec = _simulation.p.inflationElec
const autoprod = _simulation.p.autoprod
const productible = _simulation.p.productible
const puissance = _simulation.p.puissance
const degradation = _simulation.p.degradation
const _consoMoyenneFoyer = _simulation.p.consoMoyenneFoyer
for (let i of document.body.children) {
if (i.id == 'grainSaveModal') {
document.body.removeChild(i)
}
}
let modal = document.getElementById('grainSaveModal')
console.log('mount modal', modal)
if (modal) {
document.body.appendChild(modal)
}
if (!window.bootstrap) {
console.log('no window bootstrap, load')
window.bootstrap = await import('bootstrap')
}
menuElt = document.getElementById('menuoff')
if (!menuElt) {
return
}
menu = new window.bootstrap.Offcanvas(menuElt)
const popoverTriggerList = document.querySelectorAll(
'[data-bs-toggle="popover"]'
)
for (let t of popoverTriggerList) {
new window.bootstrap.Popover(t)
}
})
let saveModal_: null | Modal = null
let menu: null | Offcanvas = null
let menuElt: null | HTMLElement = null
async function saveModal(_e: Event) {
console.log('saveModal', _e)
if (!saveModal_) {
console.log('no saveModal')
saveModal_ = new window.bootstrap.Modal('#grainSaveModal', {
focus: false,
keyboard: true,
})
}
showSaveModalAfterMenuHide()
menu?.hide()
}
function showSaveModalAfterMenuHide() {
// menuElt?.removeEventListener('hidden.bs.offcanvas', showSaveModalAfterMenuHide)
let c = document.getElementsByClassName('offcanvas-backdrop')
for (const elt of c) {
elt.parentElement?.removeChild(elt)
}
saveModal_?.show()
}
let save: SubmitFunction = async ({ formData }) => {
console.log('save called')
formData.set('data', JSON.stringify(p))
formData.set('prms', JSON.stringify(data.prms))
formData.set('centrales', JSON.stringify([centrale]))
saveModal_?.hide()
return async ({ result }) => {
console.log(JSON.stringify(result))
await applyAction(result)
}
}
let expert = $page.url.hash == '#expert'
$: compteActive = $page.url.hash == '#compte' ? 'active' : ''
$: descriptionActive = $page.url.hash == '#description' ? 'active' : ''
$: banqueActive = $page.url.hash == '#banque' ? 'active' : ''
$: expertActive = expert && $page.url.hash == '#expert' ? 'active' : ''
$: simuActive =
(true || !data.guest) && $page.url.hash == '#simu' ? 'active' : ''
$: syntheseActive =
!compteActive &&
!banqueActive &&
!simuActive &&
!expertActive &&
!descriptionActive
? 'active'
: ''
$: compteShow = compteActive ? 'show' : ''
$: banqueShow = banqueActive ? 'show' : ''
$: descriptionShow = descriptionActive ? 'show' : ''
$: simuShow = simuActive ? 'show' : ''
$: syntheseShow = syntheseActive ? 'show' : ''
$: expertShow = expertActive ? 'show' : ''
let N = 30
let an = new Date().getFullYear()
let an = new Date().getFullYear()
function tarifSurplus_(puissance: number): number {
let resultat = p.tarifCRE.prix[0]
for (let i = 0; i < p.tarifCRE.limites.length; i++) {
if (puissance < p.tarifCRE.limites[i]) {
return resultat
} else {
resultat = p.tarifCRE.prix[i]
}
}
return resultat
}
$: investissement = p.puissance
? p.puissance * 1000 * p.ratio + p.enedis
: 0
$: apport = Math.min(investissement, p.apport)
$: primeInvestissement =
(p.puissance <= 3 ? 0.30
:p.puissance <= 9 ? 0.23
:p.puissance <= 36 ? 0.20
: p.puissance <= 100 ? 0.10
: 0) * 1000 * p.puissance
$: productionAnnuelle = (i: number) => H.productionAnnuelle(p, i)
let surface_kwc = 4.5
let surface = surface_kwc * (p?.puissance || 0)
function onTauxChange(ev: Event) {
let t = ev.target as HTMLInputElement
console.log(ev, t.valueAsNumber)
if (t.valueAsNumber > 100) {
t.valueAsNumber = 100
}
}
function onSurfaceChange(ev: Event) {
surface = (ev.target as HTMLInputElement).valueAsNumber
p.puissance = surface / surface_kwc
}
function onRatioChange(ev: Event) {
surface_kwc = (ev.target as HTMLInputElement).valueAsNumber
surface = p.puissance * surface_kwc
}
function onPuissanceChange(ev: Event) {
p.puissance = (ev.target as HTMLInputElement).valueAsNumber
surface = p.puissance * surface_kwc
}
$: autoconso = p.autoconso / 100
$: tarifSurplus = (i: number) =>
tarifSurplus_(p.puissance) * Math.pow(1 + p.tarifCRE.inflation / 100, i)
$: tarifLocal = (i: number) => H.tarifLocal(p, i)
$: tarifAllo = (i: number) =>
p.coutElec * Math.pow(1 + p.inflationElec / 100, i)
$: chiffreAffaires = (i: number) =>
(i == 0 ? primeInvestissement : 0) +
productionAnnuelle(i) *
(autoconso * tarifLocal(i) +
(1 - autoconso) * tarifSurplus(i) +
(p.autoprod * tarifAllo(i)) / 100)
$: exploitation = (i: number) =>
p.puissance * p.maintenance * Math.pow(1 + p.inflation / 100, i)
$: turpe = (i: number) =>
(p.puissance * p.turpe +
(1 - autoconso) * productionAnnuelle(i) * p.turpeInjection) *
Math.pow(1 + p.inflation / 100, i)
$: assurance = (i: number) =>
((investissement * p.assurance) / 100) *
Math.pow(1 + p.inflation / 100, i)
$: divers = (i: number) =>
p.divers * p.puissance * Math.pow(1 + p.inflation / 100, i)
$: charges = (i: number) =>
(investissement && i == 0 ? p.fraisDivers : 0) +
exploitation(i) +
turpe(i) +
assurance(i) +
divers(i)
$: valeurAjoutee = (i: number) => chiffreAffaires(i) - charges(i)
// Impots
// Accise: taux dérogatoire d'1€/MWh jusqu'au 31/1/2025
$: accise = (i: number) => productionAnnuelle(i) * 0.001
$: ifer = (_: number) =>
p.puissance < 100 ? 0 : 3.394 * p.puissance * p.ratioDcAc
// Exonération de taxe foncière: https://www.legifrance.gouv.fr/codes/article_lc/LEGIARTI000044996150
$: taxeFonciere = (_: number) => 0
// Formule compliquée, exonéré sur les cas concrets
$: cfe = (_: number) => 0
$: impots = (i: number) => ifer(i) + taxeFonciere(i) + cfe(i)
// EBE
$: ebe = (i: number) => valeurAjoutee(i) - impots(i)
// Déductions fiscales
$: amortissement = (i: number) =>
i < p.dureeAmortissement ? investissement / p.dureeAmortissement : 0
$: provisionOnduleurs = (i: number) =>
(p.puissance *
1000 *
p.provisionOnduleurs *
Math.pow(1 + p.inflation / 100, i)) /
10
let remboursementAnnuel: Array<number> = []
let interetsAnnuels: Array<number> = []
$: dette = Math.max(0, investissement - apport)
$: interetsDetteSenior = (i: number) =>
i < interetsAnnuels.length ? interetsAnnuels[i] : 0
$: interetsDSRA = (i: number) => (interetsDetteSenior(i) * p.tauxDSRA) / 100
$: tresorerieDisponibleDetteSenior = (i: number) => ebe(i) - is(i)
$: serviceDetteSenior = (i: number) =>
interetsDSRA(i) + interetsDetteSenior(i) + remboursementAnnuel[i]
$: dscr = (i: number) =>
i < remboursementAnnuel.length
? tresorerieDisponibleDetteSenior(i) / serviceDetteSenior(i)
: 0
$: resultatFiscal = (i: number) =>
ebe(i) -
amortissement(i) -
provisionOnduleurs(i) -
interetsDetteSenior(i) -
interetsDSRA(i)
$: is = (i: number) =>
resultatFiscal(i) <= 0 ? 0 : (resultatFiscal(i) * p.tauxIS) / 100
$: resultatNet = (i: number) => resultatFiscal(i) - is(i)
for (let i = 0; i < N; i++) {
cout +=
p.coutElec *
Math.pow(1 + p.inflationElec / 100, i) *
consoMoyenneFoyer(i)
for (let i = 0; i < $N; i++) {
cout += $_coutElec * Math.pow(1 + $inflationElec / 100, i) * consoMoyenneFoyer(i)
(tarifLocal(i) * consoMoyenneFoyer(i) * p.autoprod) / 100 +
(p.coutElec *
Math.pow(1 + p.inflationElec / 100, i) *
consoMoyenneFoyer(i) *
(100 - p.autoprod)) /
($tarifLocal[i] * consoMoyenneFoyer(i) * $autoprod) / 100 +
($_coutElec *
Math.pow(1 + $inflationElec / 100, i) *
consoMoyenneFoyer(i) *
(100 - $autoprod)) /
let totalRembAnnuel_n = remboursementAnnuel
.slice(0, n)
.reduce((a, b) => a + b, 0)
let totalInteret_n = sum(interetsDetteSenior, n)
let totalRembAnnuel = remboursementAnnuel.reduce((a, b) => a + b, 0)
let totalInteret = sum(interetsDetteSenior, n)
let totalRembAnnuel_n = $banque.remboursementAnnuel
.slice(0, n)
.reduce((a, b) => a + b, 0)
let totalInteret_n = sum_($interetsDetteSenior, n)
let totalRembAnnuel = $banque.remboursementAnnuel.reduce((a, b) => a + b, 0)
let totalInteret = sum_($interetsDetteSenior, n)
<div class="container-lg p-3 pb-5 overflow-auto">
<div class="d-flex flex-lg-row flex-column">
<nav class="navbar navbar-expand-lg start-0 w-lg-25">
<button
class="navbar-toggler"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#menuoff"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon" />
</button>
<div class="d-lg-none ms-3 me-auto">
{#if data.nom}
<h1 class="my-0">{data.nom}</h1>
{/if}
</div>
</nav>
<div
class="offcanvas-lg offcanvas-start mb-auto offmenu"
data-bs-scroll="true"
tabindex="-1"
id="menuoff">
<div class="offcanvas-body off-padding-md">
<div class="p-3 p-lg-0 overflow-auto">
{#if data.nom}
<h1>{data.nom}</h1>
{/if}
<div class="my-3 form-check form-switch">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="expertsw"
bind:checked={expert} />
<label class="form-check-label" for="expertsw"
>Niveau expert</label>
</div>
<h2 class="mt-5">Communauté</h2>
<div class="my-3">
<label class="form-label" for="puissance"
>Puissance totale (kWc)</label>
<input
class="form-control form-control-sm"
type="number"
id="puissance"
min="1"
max="3000"
value={p.puissance}
on:change={onPuissanceChange} />
{#if p.puissance > 3000}<span class="text-warning">
<i
class="bi bi-exclamation-circle-fill me-2" /> Une
communauté d'autoconsommation ne peut dépasser
plus de 3MWc. Les résultats obtenus seront donc
purement hypothétiques.</span
>{/if}
</div>
<div class="my-3">
<label class="form-label" for="surface"
>Surface totale (m²)<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="Estimation de la surface de modules photovoltaïques en fonction de la puissance et du ratio (configurable en mode expert)."
><i class="bi bi-question-circle" /></button></label>
<input
class="form-control form-control-sm"
type="number"
id="surface"
min="1"
value={surface}
on:change={onSurfaceChange} />
</script>
<div class="tab-content" id="syntheseTabContent">
<div
class="tab-pane fade show active p-3"
id="synthese-tab-pane"
role="tabpanel"
aria-labelledby="synthese-tab"
tabindex="0">
<h3 class="my-3 text-body fs-1 fw-bold">
En un regard, <span class="text-primary">tout</span> votre
projet.
</h3>
<div class="my-5 py-2 total">
<h5>Synthèse durée totale {$N} ans</h5>
<div class="row px-4">
<div class="d-flex justify-content-between p-5">
<div class="flex-column text-center">
<div class="donnee">
{format(sum_($chiffreAffaires, $N))}
</div>
<div>Chiffre d'affaire</div>
<div class="mb-3">
<label class="form-label" for="autoconso"
>Taux d'autoconsommation (%)<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="Pourcentage de la production consommée localement, à valider plus finement dans l'onglet Simulation. C'est la valeur que l'application Coturnix optimise."
><i class="bi bi-question-circle" /></button>
</label>
<div class="d-flex">
<input
class="form-range me-4"
type="range"
id="autoconso"
bind:value={p.autoconso} />{p.autoconso}%
<div class="mx-3 flex-column text-center">
<div class="donnee">
{format(sum_($charges, $N))}
<div class="mb-3">
<label class="form-label" for="autoprod"
>Taux d'autoproduction (%)<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="Pourcentage des consommations venant de sources locales, à valider plus finement dans l'onglet Simulation."
><i class="bi bi-question-circle" /></button
></label>
<div class="d-flex">
<input
class="form-range me-4"
type="range"
id="autoprod"
bind:value={p.autoprod} />{p.autoprod}%
<div class="flex-column text-center">
<div class="donnee">
{format(sum_($resultatNet, $N))}
</div>
<div class="mb-3">
<label class="form-label" for="prix"
>Prix d'achat fournisseur (€/kWh)<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="Prix d'achat moyen des consommateurs de la communauté à leur fournisseur d'électricité habituel (par exemple EDF)."
><i class="bi bi-question-circle" /></button
></label>
<input
class="form-control form-control-sm"
type="number"
step="0.01"
min="0"
id="coutElec"
bind:value={p.coutElec} />
<div>Résultat net</div>
<div class="mb-3">
<label class="form-label" for="prix"
>Prix de vente local (€/kWh)<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="Hypothèse sur le prix de vente moyen. Dans Coturnix, les producteurs et les consommateurs choisissent leurs prix acceptables, et Coturnix calcule à chaque instant le prix qui maximise la valeur totale créée par la communauté."
><i class="bi bi-question-circle" /></button
></label>
<input
class="form-control form-control-sm"
type="number"
step="0.01"
min="0"
id="prix"
bind:value={p.prix} />
</div>
<div class="mb-3">
<label class="form-label" for="echeances"
>Durée d'analyse (an)</label>
<input
class="form-control form-control-sm"
type="number"
id="echeances"
min="0"
bind:value={N} />
</div>
</div>
</div>
<h2 class="mt-5">Financement</h2>
<div class="my-3">
<label class="form-label" for="apport"
>Apport initial (€)</label>
<input
class="form-control form-control-sm"
type="number"
id="apport"
min="0"
step="1000"
bind:value={p.apport} />
<div class="row px-4">
<div class="d-flex justify-content-between p-5">
<div class="flex-column text-center">
<div class="donnee">
{format($investissement)}
</div>
<div>Montant de l'investissement</div>
<div class="mb-3">
<label class="form-label" for="interet"
>Taux d'intérêt(%)</label>
<input
class="form-control form-control-sm"
type="number"
step="0.1"
id="interet"
min="0"
max="100"
on:change={onTauxChange}
on:keyup={onTauxChange}
bind:value={p.interet} />
<div class="mx-3 flex-column text-center">
<div class="donnee">
{rentabiliteTaux(sum_($resultatNet, $N))}
</div>
<div>Retour sur investissement</div>
<div class="mb-3">
<label class="form-label" for="echeances"
>Nombre d'échéances (en mois)</label>
<input
class="form-control form-control-sm"
type="number"
id="echeances"
min="0"
bind:value={p.echeances} />
<div class="flex-column text-center">
<div class="donnee">{roiDate()}</div>
<div>Point de rentabilité</div>
{#if data.id}
<form id="delForm" method="POST" action="?/del" />
<button
class="my-2 me-2 btn btn-primary"
on:click={saveModal}>Sauvegarder</button>
<button
class="my-2 btn btn-outline-primary"
form="delForm">Supprimer</button>
{:else if data.email}
<button
class="my-2 btn btn-primary"
on:click={saveModal}>Sauvegarder</button>
{/if}
<div class="ps-lg-5 w-100 ms-0 z0">
<ul class="mt-5 mt-lg-3 nav nav-tabs" id="tabs" role="tablist">
<li class="nav-item" role="presentation">
<a
href="#synthese"
class="nav-link {syntheseActive}"
id="synthese-tab"
type="button"
role="tab"
aria-controls="synthese-tab-pane"
aria-selected="true">Synthèse</a>
</li>
<li class="nav-item" role="presentation">
<a
href="#compte"
class="nav-link {compteActive}"
id="profile-tab"
type="button"
role="tab"
aria-controls="compte-tab-pane"
aria-selected="false">Détails financiers</a>
</li>
<li class="nav-item" role="presentation">
<a
href="#description"
class="nav-link {descriptionActive}"
id="profile-tab"
type="button"
role="tab"
aria-controls="description-tab-pane"
aria-selected="false">Description</a>
</li>
<li class="nav-item" role="presentation">
<a
href="#banque"
class="nav-link {banqueActive}"
id="banque-tab"
type="button"
role="tab"
aria-controls="banque-tab-pane"
aria-selected="false">Emprunt</a>
</li>
{#if expert}
<li class="nav-item" role="presentation">
<a
href="#expert"
class="nav-link {expertActive}"
id="expert-tab"
type="button"
role="tab"
aria-controls="expert-tab-pane"
aria-selected="false">Mode expert</a>
</li>
{/if}
{#if true || !data.guest}
<li class="nav-item" role="presentation">
<a
href="#simu"
class="nav-link {simuActive}"
id="prm-tab"
type="button"
role="tab"
aria-controls="prm-tab-pane"
aria-selected="false">Simulation</a>
</li>
{/if}
</ul>
<div class="tab-content" id="syntheseTabContent">
<div
class="tab-pane fade {syntheseActive} {syntheseShow} p-3"
id="synthese-tab-pane"
role="tabpanel"
aria-labelledby="synthese-tab"
tabindex="0">
<h3 class="my-3 text-body fs-1 fw-bold">
En un regard, <span class="text-primary">tout</span> votre
projet.
</h3>
<div class="my-5 py-2 total">
<h5>Synthèse durée totale {N} ans</h5>
<div class="row px-4">
<div class="d-flex justify-content-between p-5">
<div class="flex-column text-center">
<div class="donnee">
{format(sum(chiffreAffaires, N))}
</div>
<div>Chiffre d'affaire</div>
</div>
<div class="mx-3 flex-column text-center">
<div class="donnee">
{format(sum(charges, N))}
</div>
<div>Charges d'exploitation</div>
</div>
<div class="flex-column text-center">
<div class="donnee">
{format(sum(resultatNet, N))}
</div>
<div>Résultat net</div>
</div>
</div>
</div>
<div class="row px-4">
<div class="d-flex justify-content-between p-5">
<div class="flex-column text-center">
<div class="donnee">
{format(investissement)}
</div>
<div>Montant de l'investissement</div>
</div>
<div class="mx-3 flex-column text-center">
<div class="donnee">
{rentabiliteTaux(sum(resultatNet, N))}
</div>
<div>Retour sur investissement</div>
</div>
<div class="flex-column text-center">
<div class="donnee">{roiDate()}</div>
<div>Point de rentabilité</div>
</div>
</div>
</div>
<div class="row flex-row-reverse px-4">
<div class="d-flex justify-content-around p-5">
<div
class="d-flex flex-column justify-content-between text-center me-3">
<div class="py-3 my-auto">
<svg
viewBox="{Math.floor(
-N * 0.05
)} {Math.floor(
-maxa - margin
)} {Math.ceil(N * 1.1)} {Math.ceil(
maxa - mina + 2 * margin
)}"
xmlns="http://www.w3.org/2000/svg"
width="250px"
height="100px"
preserveAspectRatio="none">
<path
d={traceArray(resultatNet, N)}
fill="transparent"
stroke-width="2"
vector-effect="non-scaling-stroke"
stroke="#b71515" />
</svg>
</div>
<div>Résultat net</div>
</div>
<div
class="d-flex flex-column justify-content-between text-center ms-3">
<div
style="width:250px"
class="py-3 my-auto">
<div
class="spinner-border ms-3 loading"
class:d-none={!pvgis_loading} />
<svg
class:d-none={pvgis_loading ||
!maxProd}
viewBox="-10 0 270 {maxProd}"
preserveAspectRatio="none"
xmlns="http://www.w3.org/2000/svg"
width="250px"
height="100px">
{#each prodWeekly as p, i}
<path
d="M {i *
5}, {maxProd} L {i *
5}, {maxProd - p}"
fill="transparent"
stroke-width="2"
stroke="#ffbf00" />
{/each}
</svg>
<svg
class="donnee"
class:d-none={pvgis_loading ||
maxProd}
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
fill="currentColor"
viewBox="0 0 16 16">
<path
d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278" />
</svg>
</div>
<div>Production annuelle</div>
</div>
</div>
<div class="row flex-row-reverse px-4">
<div class="d-flex justify-content-around p-5">
<div
class="d-flex flex-column justify-content-between text-center me-3">
<div class="py-3 my-auto">
<svg
viewBox="{Math.floor(
-$N * 0.05
)} {Math.floor(
-maxa - margin
)} {Math.ceil($N * 1.1)} {Math.ceil(
maxa - mina + 2 * margin
)}"
xmlns="http://www.w3.org/2000/svg"
width="250px"
height="100px"
preserveAspectRatio="none">
<path
d={traceArray($resultatNet, $N)}
fill="transparent"
stroke-width="2"
vector-effect="non-scaling-stroke"
stroke="#b71515" />
</svg>
<div class="row flex-row-reverse px-4">
<div class="d-flex justify-content-around p-5">
<div class="me-3 flex-column text-center">
<div class="donnee">
{N
? format(sum(resultatNet, N) / N)
: '0€'}
</div>
<div>Bénéfice annuel moyen</div>
</div>
<div class="ms-3 flex-column text-center">
<div class="donnee">
{#if coutElec() - coutElecAuto() > 0}
{format(
coutElec() - coutElecAuto()
)}
({economieFoyerMois} mois)
{:else}
Aucune 😢
{/if}
</div>
<div>Economies consommateur</div>
</div>
</div>
<div
class="d-flex flex-column justify-content-between text-center ms-3">
<div
style="width:250px"
class="py-3 my-auto">
<div
class="spinner-border ms-3 loading"
class:d-none={!pvgis_loading} />
<svg
class:d-none={pvgis_loading ||
!maxProd}
viewBox="-10 0 270 {maxProd}"
preserveAspectRatio="none"
xmlns="http://www.w3.org/2000/svg"
width="250px"
height="100px">
{#each prodWeekly as p, i}
<path
d="M {i *
5}, {maxProd} L {i *
5}, {maxProd - p}"
fill="transparent"
stroke-width="2"
stroke="#ffbf00" />
{/each}
</svg>
<svg
class="donnee"
class:d-none={pvgis_loading ||
maxProd}
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
fill="currentColor"
viewBox="0 0 16 16">
<path
d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278" />
</svg>
<div class="my-5 py-2 dix">
<h5>Synthèse 10 ans</h5>
<div class="row flex-row-reverse px-4">
<div class="d-flex justify-content-between p-5">
<div class="flex-column text-center">
<div class="donnee">
{format(sum(chiffreAffaires, 10))}
</div>
<div>Chiffre d'affaire</div>
</div>
<div class="mx-3 flex-column text-center">
<div class="donnee">
{format(sum(charges, 10))}
</div>
<div>Coût total</div>
</div>
<div class="flex-column text-center">
<div class="donnee">
{format(sum(resultatNet, 10))}
</div>
<div>Résultat net</div>
</div>
</div>
<div class="row flex-row-reverse px-4">
<div class="d-flex justify-content-around p-5">
<div class="me-3 flex-column text-center">
<div class="donnee">
{$N
? format(sum_($resultatNet, $N) / $N)
: '0€'}
<div class="row flex-row-reverse px-4 dix">
<div class="d-flex justify-content-around p-5">
<div
class="flex-column justify-content-between text-center">
<div class="donnee">{rembPret(10)}%</div>
<div>Prêt remboursé</div>
</div>
<div
class="flex-column justify-content-between text-center">
<div class="donnee">
{rentabiliteTaux(sum(resultatNet, 10))}
</div>
<div>Rentabilité</div>
</div>
</div>
<div>Bénéfice annuel moyen</div>
</div>
<div class="ms-3 flex-column text-center">
<div class="donnee">
{#if coutElec() - coutElecAuto() > 0}
{format(
coutElec() - coutElecAuto()
)}
({economieFoyerMois} mois)
{:else}
Aucune 😢
{/if}
<div class="my-5 py-2 cinq">
<h5>Synthèse 5 ans</h5>
<div class="row flex-row-reverse px-4">
<div class="d-flex justify-content-between p-5">
<div class="flex-column text-center">
<div class="donnee">
{format(sum(chiffreAffaires, 5))}
</div>
<div>Chiffre d'affaire</div>
</div>
<div class="mx-3 flex-column text-center">
<div class="donnee">
{format(sum(charges, 5))}
</div>
<div>Coût total</div>
</div>
<div class="flex-column text-center">
<div class="donnee">
{format(sum(resultatNet, 5))}
</div>
<div>Résultat net</div>
</div>
</div>
<div class="my-5 py-2 dix">
<h5>Synthèse 10 ans</h5>
<div class="row flex-row-reverse px-4">
<div class="d-flex justify-content-between p-5">
<div class="flex-column text-center">
<div class="donnee">
{format(sum_($chiffreAffaires, 10))}
<div class="row flex-row-reverse px-4">
<div class="d-flex justify-content-around p-5">
<div class="flex-column text-center">
<div class="donnee">{rembPret(5)}%</div>
<div>Prêt remboursé</div>
</div>
<div>Chiffre d'affaire</div>
</div>
<div class="flex-column text-center">
<div class="donnee">
{rentabiliteTaux(sum(resultatNet, 5))}
</div>
<div>Rentabilité</div>
</div>
</div>
<div class="mx-3 flex-column text-center">
<div class="donnee">
{format(sum_($charges, 10))}
<div class="my-5 py-2 moyenne">
<h5>Synthèse année moyenne</h5>
<div class="row flex-row flex-wrap px-4">
<div class="d-flex justify-content-between py-5">
<div class="flex-column text-center mx-3">
<div class="donnee">
{format(sum(resultatNet, N) / N)}
</div>
<div>Bénéfices moyen</div>
</div>
<div class="flex-column text-center mx-3">
<div class="donnee">
{format(sum(chiffreAffaires, N) / N)}
</div>
<div>Chiffre d'affaire</div>
</div>
<div class="flex-column text-center mx-3">
<div class="donnee">
{fmt2.format(
(investissement
? (sum(resultatNet, N) * 100) /
investissement -
100
: 0) / N
)}%
</div>
<div>Rentabilité annuelle</div>
</div>
<div class="flex-column text-center mx-3">
<div class="donnee">
{#if coutElec() - coutElecAuto() > 0}
{fmt0.format(
((coutElec() - coutElecAuto()) *
100) /
coutElec()
)}%
{:else}
Aucune 😢
{/if}
</div>
<div>Économies consommateur</div>
</div>
</div>
<div class="flex-column text-center">
<div class="donnee">
{format(sum_($resultatNet, 10))}
<div
class="tab-pane fade {compteActive} {compteShow} p-3"
id="compte-tab-pane"
role="tabpanel"
aria-labelledby="compte-tab"
tabindex="0">
<h3 class="my-3 fs-1 fw-bold">
<span class="text-primary">Projetez-vous</span> dans votre
projet.
</h3>
Durée d'analyse : {N} ans
<h4 class="my-3 mt-5">Résultat</h4>
<div class="ms-3 row">
<h5 class="mt-5">Résultat net</h5>
<div class="fw-bold fs-4">
Cumulé : <span class="text-primary"
>{format(sum(resultatNet, N))}</span>
</div>
<div class="d-flex flex-wrap">
<div class="d-flex flex-column my-3">
<div class="titreligne tcellt">Année</div>
<div class="titreligne tcellt">
Résultat net
</div>
</div>
{#each Array(Math.ceil(N)) as _, i}
<div class="d-flex flex-column my-3">
<div class="tcellt">{an + i}</div>
<div class="tcellt">
{resultatNet(i).toFixed(arr)}
</div>
</div>
{/each}
</div>
<div class="row flex-row-reverse px-4 dix">
<div class="d-flex justify-content-around p-5">
<div
class="flex-column justify-content-between text-center">
<div class="donnee">{rembPret(10)}%</div>
<div>Prêt remboursé</div>
<h4 class="my-3">Détail</h4>
<div class="ms-3">
<h5 class="mt-5">Chiffre d'affaires</h5>
<div class="fw-bold fs-4">
Cumulé : <span class="text-primary"
>{format(sum(chiffreAffaires, N))}</span>
</div>
<div class="d-flex flex-wrap">
<div class="d-flex flex-column my-4">
<div class="titreligne tcellt">Année</div>
<div class="titreligne tcellt">
Autoproduction (€)
</div>
<div class="titreligne tcellt">
Vente locale (€)
</div>
<div class="titreligne tcellt">
Vente de surplus (€)
</div>
<div class="titreligne tcellt">Prime</div>
<div class="titreligne tcellt">
Chiffre d'affaires
</div>
</div>
{#each Array(Math.ceil(N)) as _, i}
<div class="d-flex flex-column my-4">
<div class="tcellt">{an + i}</div>
<div class="tcellt">
{(
productionAnnuelle(i) *
((p.autoprod * tarifAllo(i)) / 100)
).toFixed(0)}
</div>
<div class="tcellt">
{(
productionAnnuelle(i) *
(autoconso * tarifLocal(i))
).toFixed(0)}
</div>
<div class="tcellt">
{(
productionAnnuelle(i) *
((1 - autoconso) * tarifSurplus(i))
).toFixed(0)}
</div>
<div class="tcellt">{#if i == 0}{primeInvestissement}{:else} {/if}</div>
<div class="tcellt">
{chiffreAffaires(i).toFixed(arr)}
</div>
</div>
{/each}
</div>
<h5 class="my-3">Charges d'exploitation</h5>
<div class="fw-bold fs-4">
Cumulé : <span class="text-primary"
>{format(sum(charges, N))}</span>
<div
class="flex-column justify-content-between text-center">
<div class="donnee">
{rentabiliteTaux(sum_($resultatNet, 10))}
<div class="d-flex flex-wrap">
<div class="d-flex flex-column my-4">
<div class="titreligne tcellt">Année</div>
<div class="titreligne tcellt">
Maintenance (€)
</div>
<div class="titreligne tcellt">TURPE (€)</div>
<div class="titreligne tcellt">
Assurance (€)
</div>
<div class="titreligne tcellt">Divers</div>
<div class="titreligne tcellt">
Total charges
</div>
</div>
{#each Array(Math.ceil(N)) as _, i}
<div class="d-flex flex-column my-4">
<div class="tcellt">{an + i}</div>
<div class="tcellt">
{exploitation(i).toFixed(arr)}
</div>
<div class="tcellt">
{turpe(i).toFixed(arr)}
</div>
<div class="tcellt">
{assurance(i).toFixed(arr)}
</div>
<div class="tcellt">
{divers(i).toFixed(arr)}
</div>
<div class="tcellt">
{charges(i).toFixed(arr)}
</div>
</div>
{/each}
</div>
<div>Rentabilité</div>
</div>
</div>
</div>
</div>
<h5 class="my-3">Excédent brut d'exploitation</h5>
<div class="fw-bold fs-4">
Cumulé : <span class="text-primary"
>{format(sum(ebe, N))}</span>
<div class="my-5 py-2 cinq">
<h5>Synthèse 5 ans</h5>
<div class="row flex-row-reverse px-4">
<div class="d-flex justify-content-between p-5">
<div class="flex-column text-center">
<div class="donnee">
{format(sum_($chiffreAffaires, 5))}
<div class="d-flex flex-wrap">
<div class="d-flex flex-column my-4">
<div class="titreligne tcellt">Année</div>
<div class="titreligne tcellt">
Valeur ajoutée (€)
</div>
<div class="titreligne tcellt">Accise (€)</div>
<div class="titreligne tcellt">IFER (€)</div>
<div class="titreligne tcellt">
Taxe foncière
</div>
<div class="titreligne tcellt">
Cotisation foncière des entreprises
</div>
<div class="titreligne tcellt">
Excédent brut d'exploitation
</div>
</div>
{#each Array(Math.ceil(N)) as _, i}
<div class="d-flex flex-column my-4">
<div class="tcellt">{an + i}</div>
<div class="tcellt">
{valeurAjoutee(i).toFixed(arr)}
</div>
<div class="tcellt">
{accise(i).toFixed(arr)}
</div>
<div class="tcellt">
{ifer(i).toFixed(arr)}
</div>
<div class="tcellt">
{taxeFonciere(i).toFixed(arr)}
</div>
<div class="tcellt">
{cfe(i).toFixed(arr)}
</div>
<div class="tcellt">
{ebe(i).toFixed(arr)}
</div>
</div>
{/each}
</div>
<h5 class="my-3">Résultat net</h5>
<div class="fw-bold fs-4">
Cumulé : <span class="text-primary"
>{format(sum(resultatNet, N))}</span>
<div class="mx-3 flex-column text-center">
<div class="donnee">
{format(sum_($charges, 5))}
<div class="d-flex flex-wrap">
<div class="d-flex flex-column my-4">
<div class="titreligne tcellt">Année</div>
<div class="titreligne tcellt">Onduleurs
<button
type="button"
class="btn btn-sm btn-link align-baseline p-0 ms-2"
data-bs-toggle="popover"
data-bs-trigger="hover focus"
data-bs-content="Provision pour le remplacement des onduleurs"
><i class="bi bi-question-circle" /></button>
</div>
<div class="titreligne tcellt">
Amortissement
</div>
<div class="titreligne tcellt">
Intérêts dette senior
</div>
<div class="titreligne tcellt">
Intérêts DSRA <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="Dans un emprunt, compte bancaire qui permettra de payer le service de la dette si jamais l'entité emprunteuse n'est plus en mesure de le faire."
><i class="bi bi-question-circle" /></button>
</div>
<div class="titreligne tcellt">
Résultat fiscal
</div>
<div class="titreligne tcellt">
Impôt sur les sociétés
</div>
<div class="titreligne tcellt">
Résultat net
</div>
</div>
{#each Array(Math.ceil(N)) as _, i}
<div class="d-flex flex-column my-4">
<div class="tcellt">{an + i}</div>
<div class="tcellt">
{provisionOnduleurs(i).toFixed(arr)}
</div>
<div class="tcellt">
{amortissement(i).toFixed(arr)}
</div>
<div class="tcellt">
{interetsDetteSenior(i).toFixed(arr)}
</div>
<div class="tcellt">
{interetsDSRA(i).toFixed(arr)}
</div>
<div class="tcellt">
{resultatFiscal(i).toFixed(arr)}
</div>
<div class="tcellt">
{is(i).toFixed(arr)}
</div>
<div class="tcellt">
{resultatNet(i).toFixed(arr)}
</div>
</div>
{/each}
<div class="flex-column text-center">
<div class="donnee">
{format(sum_($resultatNet, 5))}
{#if investissement > apport}
<h5 class="my-3">Couverture de la dette</h5>
<div class="d-flex flex-wrap">
<div class="d-flex flex-column my-3">
<div class="titreligne tcellt">Année</div>
<div class="titreligne tcellt">DSCR</div>
</div>
{#each Array(Math.ceil(N)) as _, i}
<div class="d-flex flex-column my-3">
<div class="tcellt">{an + i}</div>
<div class="tcellt">
{fmt2.format(dscr(i))}
</div>
</div>
{/each}
</div>
{/if}
<div>Résultat net</div>
<div
class="tab-pane fade {descriptionActive} {descriptionShow} p-3"
id="description-tab-pane"
role="tabpanel"
aria-labelledby="description-tab"
tabindex="0">
<h3 class="my-3 fs-1 fw-bold">
Quelques <span class="text-primary">détails</span> de plus.
</h3>
{#if descriptionActive}
<Description
bind:zoom
bind:position={centrale.position}
bind:azimuth={centrale.azimuth}
bind:inclinaison={centrale.inclinaison} />
{/if}
</div>
<div
class="tab-pane fade {banqueActive} {banqueShow} p-3"
id="banque-tab-pane"
role="tabpanel"
aria-labelledby="banque-tab"
tabindex="0">
<h3 class="my-3 fs-1 fw-bold">
Tous <span class="text-primary">les recoins</span> de votre
emprunt.
</h3>
<Banque
{dette}
taux={p.interet / 100}
duree={p.echeances}
bind:interetsAnnuels
bind:remboursementAnnuel />
</div>
{#if expert}
<div
class="tab-pane fade {expertActive} {expertShow} p-3"
id="expert-tab-pane"
role="tabpanel"
aria-labelledby="expert-tab"
tabindex="0">
<div style="max-width:300px">
<div class="mb-3">
<label class="form-label" for="enedis"
>Coût de raccordement Enedis</label>
<input
class="form-control form-control-sm"
type="number"
id="enedis"
min="0"
bind:value={p.enedis} />
</div>
<div class="mb-3">
<label class="form-label" for="productible"
>Productible annuel (kWh/kWc)</label>
<input
class="form-control form-control-sm"
type="number"
id="productible"
min="0"
bind:value={p.productible} />
</div>
<div class="row flex-row-reverse px-4">
<div class="d-flex justify-content-around p-5">
<div class="flex-column text-center">
<div class="donnee">{rembPret(5)}%</div>
<div>Prêt remboursé</div>
</div>
<div class="mb-3">
<label class="form-label" for="ratio"
>Ratio installation (€/W)</label>
<input
class="form-control form-control-sm"
type="number"
id="ratio"
min="0"
bind:value={p.ratio} />
</div>
<div class="mb-3">
<label class="form-label" for="degradation"
>Taux de dégradation PV(%)</label>
<input
class="form-control form-control-sm"
type="number"
id="degradation"
min="0"
bind:value={p.degradation} />
</div>
<div class="mb-3">
<label class="form-label" for="dsra"
>Taux d'intérêt DSRA (%)</label>
<input
class="form-control form-control-sm"
type="number"
id="dsra"
min="0"
step="any"
bind:value={p.tauxDSRA} />
</div>
<div class="mb-3">
<label class="form-label" for="inflation"
>Inflation tarif local</label>
<input
class="form-control form-control-sm"
type="number"
id="inflation"
min="0"
bind:value={p.inflation} />
</div>
<div class="mb-3">
<label class="form-label" for="inflationElec"
>Inflation nationale de l'électricité (%)</label>
<input
class="form-control form-control-sm"
type="number"
bind:value={p.inflationElec}
min="0"
id="inflationElec" />
</div>
<div class="my-3">
<label class="form-label" for="surfacekwc">Surface d'un kWc (m²)</label>
<input
class="form-control form-control-sm"
type="number"
id="surfacekwc"
step ="any"
value={surface_kwc}
on:change={onRatioChange} />
</div>
<div class="flex-column text-center">
<div class="donnee">
{rentabiliteTaux(sum_($resultatNet, 5))}
{/if}
</div>
<div class="tab-content" id="simuTabContent">
<div
class="tab-pane fade {simuActive} {simuShow} show p-3"
id="simu-tab-pane"
role="tabpanel"
aria-labelledby="simu-tab"
tabindex="0">
<h4 class="my-3">Simulation</h4>
<Simulation
id={data.id}
{server}
{assets}
{prmsFeature}
position={centrale.position}
azimuth={centrale.azimuth}
inclinaison={centrale.inclinaison}
bind:prms={data.prms}
puissance={p.puissance}
bind:prodWeekly
bind:maxProd
bind:pvgis_loading />
</div>
<div class="modal" id="grainSaveModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body">
<h5 class="modal-title pb-3" id="saveModalLabel">
Sauvegarder
</h5>
<form
method="POST"
id="saveForm"
action="?/save"
use:enhance={save}>
<div class="mb-3">
<label class="form-label" for="nom"
>Nom du projet</label>
<input
class="form-control"
type="text"
id="nom"
name="nom"
bind:value={data.nom}
required />
<div class="my-5 py-2 moyenne">
<h5>Synthèse année moyenne</h5>
<div class="row flex-row flex-wrap px-4">
<div class="d-flex justify-content-between py-5">
<div class="flex-column text-center mx-3">
<div class="donnee">
{format(sum_($resultatNet, $N) / $N)}
<div class="mb-3">
<label class="form-label" for="porteur"
>Prénom et nom du porteur du projet</label>
<input
class="form-control"
type="text"
id="porteur"
name="porteur"
bind:value={data.porteur} />
</div>
<div class="mb-3">
<label class="form-label" for="adresse"
>Adresse</label>
<input
class="form-control"
type="text"
id="adresse"
name="adresse"
bind:value={data.adresse} />
<div>Bénéfices moyen</div>
</div>
<div class="flex-column text-center mx-3">
<div class="donnee">
{format(sum_($chiffreAffaires, $N) / $N)}
<div class="mb-3">
<label class="form-label" for="tel"
>Téléphone</label>
<input
class="form-control"
type="text"
id="tel"
name="tel"
bind:value={data.tel} />
<div>Chiffre d'affaire</div>
</div>
<div class="flex-column text-center mx-3">
<div class="donnee">
{fmt2.format(
($investissement
? (sum_($resultatNet, $N) * 100) /
$investissement -
100
: 0) / $N
)}%
<div class="mb-3">
<label class="form-label" for="photo">Photo</label>
<input
class="form-control"
type="file"
id="photo"
name="photo" />
<div>Rentabilité annuelle</div>
</div>
<div class="flex-column text-center mx-3">
<div class="donnee">
{#if coutElec() - coutElecAuto() > 0}
{fmt0.format(
((coutElec() - coutElecAuto()) *
100) /
coutElec()
)}%
{:else}
Aucune 😢
{/if}
<div class="modal-footer">
<button form="saveForm" class="btn btn-primary"
>Sauvegarder</button>
<button
type="button"
class="btn btn-outline-primary"
data-bs-dismiss="modal">Annuler</button>
</div>
@media (prefers-color-scheme: light) {
nav {
background-color: #fff !important;
}
}
.offmenu {
padding-top: 40px;
}
.z0 {
z-index: 0;
}
input.prix::-webkit-outer-spin-button,
input.prix::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
input.prix[type='number'] {
-moz-appearance: textfield; /* Firefox */
}
import { error } from '@sveltejs/kit'
import type { PageServerLoad, Actions } from './$types'
import { userForm, Grain, save, del } from '../../../helpers'
export const load: PageServerLoad = async ({ params, platform }) => {
console.log('load', params['id'])
if (!platform) {
return null
}
if (params['id'] == 'nouveau') {
const sim = Grain.newSimulationCommunaute()
console.log(JSON.stringify(sim))
return <Grain.GetProjetResp & { guest?: boolean }>{
nom: '',
id: '',
porteur: '',
tel: '',
adresse: '',
simulation: { ...sim },
guest: false,
email: 'grain@coturnix.fr',
prms: [],
centrales: [],
}
}
const id_ = platform.env.USER.idFromName('grain')
const projet = await userForm(platform.env, id_, {
GetProjet: params['id'],
})
console.log('projet', JSON.stringify(projet))
if (projet) {
return <Grain.GetProjetResp>projet
}
import type { Actions } from './$types'
import { save, del } from '../../../helpers'
<script lang="ts">
import { page } from '$app/stores'
import type { Grain } from '../../../helpers'
import { writable } from 'svelte/store'
import { setContext, onMount } from 'svelte'
import { Derived } from './calcul'
import { email } from '../../../stores'
export let data: {
nom: string
porteur: string
adresse: string
tel: string
id: string
communaute?: Grain.Communaute&{isMoving?: boolean},
img?: string
simulation: Grain.SimulationCommunaute
guest?: boolean
prms: Grain.Prm[]
profils: Grain.Profil[]
email?: string
accesParticuliers?: boolean
expert: boolean
N: number
};
let simulation = new Derived(data.simulation);
setContext("simulation", simulation)
setContext("banque", simulation.banque)
setContext("communaute", writable(null))
let expert = writable(data.expert)
setContext("expert", expert)
export let root = "/grain/projet/"
$: compteActive = $page.route.id?.endsWith("/finances") ?? false
$: descriptionActive = $page.route.id?.endsWith("/description") ?? false
$: banqueActive = $page.route.id?.endsWith("/banque") ?? false
$: expertActive = $page.route.id?.endsWith("/expert") ?? false
$: simuActive = $page.route.id?.endsWith("/simulation") ?? false
$: syntheseActive = !compteActive && !descriptionActive && !banqueActive && !expertActive && !simuActive
let menuElt: null | HTMLElement = null
onMount(async () => {
email.set(data.email || null)
email.subscribe((value) => {
if (value) data.email = value
})
if (!window.bootstrap) {
console.log('no window bootstrap, load')
window.bootstrap = await import('bootstrap')
}
menuElt = document.getElementById('menuoff')
if (!menuElt) {
console.log("no menu off")
return
}
new window.bootstrap.Offcanvas(menuElt)
const popoverTriggerList = document.querySelectorAll(
'[data-bs-toggle="popover"]'
)
for (let t of popoverTriggerList) {
new window.bootstrap.Popover(t)
}
})
let surface_kwc = 4.5
let surface = surface_kwc * data.simulation.puissance
function onTauxChange(ev: Event) {
let t = ev.target as HTMLInputElement
console.log(ev, t.valueAsNumber)
if (t.valueAsNumber > 100) {
t.valueAsNumber = 100
}
}
function onSurfaceChange(ev: Event) {
if(simulation) {
surface = (ev.target as HTMLInputElement).valueAsNumber
simulation.p.puissance.set(surface / surface_kwc)
}
}
// function onRatioChange(ev: Event) {
// if(simulation) {
// surface_kwc = (ev.target as HTMLInputElement).valueAsNumber
// surface = get(simulation.p.puissance) * surface_kwc
// }
// }
function onPuissanceChange(ev: Event) {
if(simulation) {
simulation.p.puissance.set((ev.target as HTMLInputElement).valueAsNumber)
surface = $puissance * surface_kwc
}
}
const puissance = simulation?.p.puissance
const autoconso = simulation?.p.autoconso
const autoprod = simulation?.p.autoprod
const cout = simulation?.p.coutElec
const prix = simulation.p.prix
const apport = simulation.p.apport
const interet = simulation.p.interet
const echeances = simulation.p.echeances
const N = simulation.N
</script>
<div class="container-lg p-3 pb-5 overflow-auto">
<div class="d-flex flex-lg-row flex-column">
<nav class="navbar navbar-expand-lg start-0 w-lg-25">
<button
class="navbar-toggler"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#menuoff"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon" />
</button>
<div class="d-lg-none ms-3 me-auto">
<h1>{data.nom}</h1>
</div>
</nav>
<div
class="offcanvas-lg offcanvas-start mb-auto offmenu"
data-bs-scroll="true"
tabindex="-1"
id="menuoff">
<div class="offcanvas-body off-padding-md">
<div class="p-3 p-lg-0 overflow-auto">
<input type="text" class="form-control input-h1" bind:value={data.nom} placeholder="Nouveau projet" />
<form action="/projet/{data.id}/?save">
<button class="my-2 btn btn-sm btn-primary">Sauvegarder</button>
</form>
{#if data.email}
<div class="my-3 form-check form-switch">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="expertsw"
bind:checked={$expert} />
<label class="form-check-label" for="expertsw"
>Niveau expert</label>
</div>
{/if}
<h2 class="mt-5">Communauté</h2>
<div class="my-3">
<label class="form-label" for="puissance"
>Puissance totale (kWc)</label>
<input
class="form-control form-control-sm"
type="number"
id="puissance"
min="1"
max="3000"
value={$puissance}
on:change={onPuissanceChange} />
{#if data.simulation.puissance > 3000}<span class="text-warning">
<i
class="bi bi-exclamation-circle-fill me-2" /> Une
communauté d'autoconsommation ne peut dépasser
plus de 3MWc. Les résultats obtenus seront donc
purement hypothétiques.</span
>{/if}
</div>
<div class="my-3">
<label class="form-label" for="surface"
>Surface totale (m²)<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="Estimation de la surface de modules photovoltaïques en fonction de la puissance et du ratio (configurable en mode expert)."
><i class="bi bi-question-circle" /></button></label>
<input
class="form-control form-control-sm"
type="number"
id="surface"
min="1"
value={surface}
on:change={onSurfaceChange} />
</div>
<div class="mb-3">
<label class="form-label" for="autoconso"
>Taux d'autoconsommation (%)<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="Pourcentage de la production consommée localement, à valider plus finement dans l'onglet Simulation. C'est la valeur que l'application Coturnix optimise."
><i class="bi bi-question-circle" /></button>
</label>
<div class="d-flex">
<input
class="form-range me-4"
type="range"
id="autoconso"
bind:value={$autoconso} />{$autoconso}%
</div>
</div>
<div class="mb-3">
<label class="form-label" for="autoprod"
>Taux d'autoproduction (%)<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="Pourcentage des consommations venant de sources locales, à valider plus finement dans l'onglet Simulation."
><i class="bi bi-question-circle" /></button
></label>
<div class="d-flex">
<input
class="form-range me-4"
type="range"
id="autoprod"
bind:value={$autoprod} />{$autoprod}%
</div>
</div>
<div class="mb-3">
<label class="form-label" for="prix"
>Prix d'achat fournisseur (€/kWh)<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="Prix d'achat moyen des consommateurs de la communauté à leur fournisseur d'électricité habituel (par exemple EDF)."
><i class="bi bi-question-circle" /></button
></label>
<input
class="form-control form-control-sm"
type="number"
step="0.01"
min="0"
id="coutElec"
bind:value={$cout} />
</div>
<div class="mb-3">
<label class="form-label" for="prix"
>Prix de vente local (€/kWh)<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="Hypothèse sur le prix de vente moyen. Dans Coturnix, les producteurs et les consommateurs choisissent leurs prix acceptables, et Coturnix calcule à chaque instant le prix qui maximise la valeur totale créée par la communauté."
><i class="bi bi-question-circle" /></button
></label>
<input
class="form-control form-control-sm"
type="number"
step="0.01"
min="0"
id="prix"
bind:value={$prix} />
</div>
<div class="mb-3">
<label class="form-label" for="echeances"
>Durée d'analyse (an)</label>
<input
class="form-control form-control-sm"
type="number"
id="echeances"
min="0"
bind:value={$N} />
</div>
<h2 class="mt-5">Financement</h2>
<div class="my-3">
<label class="form-label" for="apport"
>Apport initial (€)</label>
<input
class="form-control form-control-sm"
type="number"
id="apport"
min="0"
step="1000"
bind:value={$apport} />
</div>
<div class="mb-3">
<label class="form-label" for="interet"
>Taux d'intérêt(%)</label>
<input
class="form-control form-control-sm"
type="number"
step="0.1"
id="interet"
min="0"
max="100"
on:change={onTauxChange}
on:keyup={onTauxChange}
bind:value={$interet} />
</div>
<div class="mb-3">
<label class="form-label" for="echeances"
>Nombre d'échéances (en mois)</label>
<input
class="form-control form-control-sm"
type="number"
id="echeances"
min="0"
bind:value={$echeances} />
</div>
{#if data.id}
<form method="POST" action="?/del">
<button class="my-2 btn btn-outline-primary">Supprimer</button>
</form>
{/if}
</div>
</div>
</div>
<div class="ps-lg-5 w-100 ms-0 z0 main">
<ul class="mt-5 mt-lg-3 nav nav-tabs" id="tabs" role="tablist">
<li class="nav-item" role="presentation">
<a
href="{root}{data.id}"
class="nav-link {syntheseActive && "active"}"
id="synthese-tab"
type="button"
role="tab"
aria-controls="synthese-tab-pane"
aria-selected="true">Synthèse</a>
</li>
<li class="nav-item" role="presentation">
<a
href="{root}{data.id}/finances"
class="nav-link {compteActive && "active"}"
id="profile-tab"
type="button"
role="tab"
aria-controls="compte-tab-pane"
aria-selected="false">Détails financiers</a>
</li>
<li class="nav-item" role="presentation">
<a
href="{root}{data.id}/description"
class="nav-link {descriptionActive && "active"}"
id="profile-tab"
type="button"
role="tab"
aria-controls="description-tab-pane"
aria-selected="false">Description</a>
</li>
<li class="nav-item" role="presentation">
<a
href="{root}{data.id}/banque"
class="nav-link {banqueActive && "active"}"
id="banque-tab"
type="button"
role="tab"
aria-controls="banque-tab-pane"
aria-selected="false">Emprunt</a>
</li>
{#if $expert}
<li class="nav-item" role="presentation">
<a
href="{root}{data.id}/expert"
class="nav-link {expertActive && "active"}"
id="expert-tab"
type="button"
role="tab"
aria-controls="expert-tab-pane"
aria-selected="false">Mode expert</a>
</li>
{/if}
{#if true || !data.guest}
<li class="nav-item" role="presentation">
<a
href="{root}{data.id}/simulation"
class="nav-link {simuActive && "active"}"
id="prm-tab"
type="button"
role="tab"
aria-controls="prm-tab-pane"
aria-selected="false">Simulation</a>
</li>
{/if}
</ul>
<slot/>
</div>
</div>
</div>
<style lang="scss">
@media (prefers-color-scheme: light) {
nav {
background-color: #fff !important;
}
}
@media (min-width: 992px) {
.offmenu {
position: fixed;
top: 80px;
height: calc(100% - 120px);
padding-bottom: 20px;
overflow: scroll;
padding-right: 20px;
width: 25%;
max-width: 280px;
}
.main {
margin-left: calc(min(25%, 280px)) !important;
padding-left: 0;
}
}
.input-h1 {
font-size: 1.5em;
font-weight: bold;
}
.z0 {
z-index: 0;
}
.loading {
color: #ffbf00;
}
.offcanvas-body h2 {
font-size: 1.2em;
}
.titreligne {
font-size: 1rem;
font-weight: bold;
}
.donnee {
font-size: 2rem;
font-weight: bold;
}
.tcellt {
border-bottom: 1px solid $primary;
}
.tcellt:last-child {
border-bottom: unset;
}
.tcellt {
padding: 8px 10px;
}
</style>
import { error } from '@sveltejs/kit'
import type { LayoutServerLoad } from './$types'
import { Grain } from '../../../helpers'
import * as fs from 'node:fs'
export const load: LayoutServerLoad = async ({ params }) => {
if (params['id'] == 'nouveau') {
const sim = Grain.newSimulationCommunaute()
console.log(JSON.stringify(sim))
return <Grain.GetProjetResp & { guest?: boolean }>{
nom: '',
id: 'nouveau',
porteur: '',
tel: '',
adresse: '',
simulation: { ...sim },
guest: false,
email: 'grain@coturnix.fr',
prms: [],
profils: [],
N: 30,
expert: false,
}
}
try {
return fs.readFileSync(params['id'], 'utf8');
} catch (err) {
console.error(err);
return error(500, `${err}`)
}
}
export type SaveFunction = (e: Event, nom: string, simulation: SimulationCommunaute, prms: Prm[], profils: Profil[], communaute?: Communaute) => Promise<void>
export type Rapport = {
id: string,
nom: string,
simulation: SimulationCommunaute,
communaute?: Communaute,
production?: number[]
charge?: Record<string, number[]>
interets_annuel: number[],
remboursement_annuel: number[],
};
export type RapportFunction = (r: Rapport) => Promise<void>
export type LatLng = { lat: number; lng: number }
export type Derogation = 'non' | '10' | '20'
export type Compteur = {
lat: number
lng: number
puissance: number
azimuth: number
inclinaison: number
}
export type Communaute = {
lat: number
lng: number
derogation: Derogation
compteurs?: Compteur[]
}
export function profils(expert: boolean) {
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" ]
},
*/
}
if(expert) {
for (let p of [
"ENT1-P1",
"ENT1-P2",
"ENT1-P3",
"ENT1-P4",
"ENT2-P1",
"ENT2-P2",
"ENT2-P3",
"ENT2-P4",
"ENT3-P1",
"ENT3-P2",
"ENT3-P3",
"ENT3-P4",
"ENT3-P5",
"ENT4-P1",
"ENT4-P2",
"ENT4-P3",
"ENT4-P4",
"ENT5-P1",
"ENT5-P2",
"ENT5-P3",
"ENT5-P4",
"ENT5-P5",
"ENT5-P6",
"ENT5-P7",
"ENT5-P8",
"ENT6-P1",
"ENT6-P2",
"ENT6-P3",
"ENT6-P4",
"ENT6-P5",
"ENT6-P6",
"ENT7-P1",
"ENT7-P2",
"ENT7-P3",
"ENT7-P4",
"ENT7-P5",
"PRD1-P1",
"PRD2-P1",
"PRD3-P1",
"PRD4-P1",
"PRO1-P1",
"PRO1WE-P1",
"PRO1WE-P2",
"PRO22WE-P1",
"PRO22WE-P2",
"PRO22WE-P3",
"PRO22WE-P4",
"PRO2-P1",
"PRO2-P2",
"PRO3-P1",
"PRO3-P2",
"PRO3-P3",
"PRO3-P4",
"PRO3-P5",
"PRO3-P6",
"PRO4-P1",
"PRO4-P2",
"PRO5-P1",
"PRO6-P1",
"PRO6-P2",
"PRO6-P3",
"PRO6-P4",
"RES11-P1",
"RES11WE-P1",
"RES11WE-P2",
"RES1-P1",
"RES1WE-P1",
"RES1WE-P2",
"RES22WE-P1",
"RES22WE-P2",
"RES22WE-P3",
"RES22WE-P4",
"RES2-P1",
"RES2-P2",
"RES2WE-P1",
"RES2WE-P2",
"RES2WE-P3",
"RES3-P1",
"RES3-P2",
"RES3-P3",
"RES3-P4",
"RES3-P5",
"RES3-P6",
"RES4-P1",
"RES4-P2",
"RES5-P1",
"RES5-P2",
"RES5-P3",
"RES5-P4",
]) {
profils_[p] = {
profils: [p]
}
}
}
return profils_;
}
export function productionAnnuelle(p: Grain.SimulationCommunaute, i: number) {
return p.productible * p.puissance * Math.pow(1 - p.degradation / 100, i)
export function productionAnnuelle(productible: number, puissance: number, degradation: number, i: number) {
return productible * puissance * Math.pow(1 - degradation / 100, i)