Etude de la frontiere de l excedent : la voiture est un delestage, pas une charge
Integration de l historique HA sur 8 jours : 64% de l energie livree au vehicule vient d un excedent reel, 16% sort de la reserve batterie. La frontiere se ferme entre 18h30 et 19h50 fin aout, franchement. Cote reserve nocturne, le bilan repond deja et il repond mal a notre avantage : sept jours sur huit, meme pleine, la batterie ne passe pas la nuit. Le deficit est structurel, ce qui est evitable c est de l aggraver. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
This commit is contained in:
@@ -599,3 +599,129 @@ Ordre de travail qui en découle, avant tout câblage :
|
|||||||
- [ ] **États zombies** `stop_source_lost` / `idle_wait_source` — noms
|
- [ ] **États zombies** `stop_source_lost` / `idle_wait_source` — noms
|
||||||
transitoires peu lisibles dans le journal et le tableau de bord (cf. HANDOFF
|
transitoires peu lisibles dans le journal et le tableau de bord (cf. HANDOFF
|
||||||
§24, hors périmètre à l'époque).
|
§24, hors périmètre à l'époque).
|
||||||
|
|
||||||
|
## 01/09/2026 — La frontière de l'excédent, mesurée sur 8 jours
|
||||||
|
|
||||||
|
Question du propriétaire : faut-il charger la voiture le soir ? Avec la règle
|
||||||
|
qu'il pose et qui gouverne désormais le sujet — **la voiture est un délestage de
|
||||||
|
l'excédent PV, pas une charge prioritaire qui vide les batteries solaires**, et
|
||||||
|
la frontière se détecte à la **puissance de décharge batterie**, pas à la
|
||||||
|
production PV ni à un seuil de SOC.
|
||||||
|
|
||||||
|
### Méthode
|
||||||
|
|
||||||
|
`tools/etude-frontiere-excedent.mjs` intègre l'historique HA (résolution native
|
||||||
|
~2 s, rétention 90 jours effective depuis le 24/08 — vérifié) et répartit
|
||||||
|
l'énergie **réellement livrée au véhicule**, instant par instant, entre trois
|
||||||
|
origines physiques :
|
||||||
|
|
||||||
|
```
|
||||||
|
batterie : min(P_evcs, décharge batterie) -> réserve entamée
|
||||||
|
réseau : min(reste, import réseau) -> énergie payante
|
||||||
|
excédent : le solde -> production qui aurait été
|
||||||
|
refusée ou simplement stockée
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune écriture, aucune décision : le script lit et intègre. Convention
|
||||||
|
d'attribution assumée : sans la voiture, la décharge batterie aurait été
|
||||||
|
réduite d'au plus `P_evcs` — l'attribution est donc une borne haute honnête de
|
||||||
|
ce que la voiture a coûté à la réserve.
|
||||||
|
|
||||||
|
### Ce que les 8 jours disent (24/08 → 31/08)
|
||||||
|
|
||||||
|
| | kWh | part |
|
||||||
|
|---|---:|---:|
|
||||||
|
| Livré au véhicule | 80,13 | 100 % |
|
||||||
|
| dont **excédent** | 51,18 | **64 %** |
|
||||||
|
| dont **batterie** | 13,22 | **16 %** |
|
||||||
|
| dont réseau (HC nuit, voulu) | 15,73 | 20 % |
|
||||||
|
|
||||||
|
Part d'excédent par heure, cumul sur la période :
|
||||||
|
|
||||||
|
| h | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | **19** | **20** |
|
||||||
|
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||||
|
| part excédent | 94 % | 99 % | 86 % | 80 % | 83 % | 90 % | 77 % | 92 % | 81 % | **38 %** | **4 %** |
|
||||||
|
|
||||||
|
**La frontière existe, elle est franche, et elle tombe entre 18 h 30 et 19 h 50**
|
||||||
|
(heure du dernier instant où le véhicule chargeait sans faire décharger la
|
||||||
|
batterie) :
|
||||||
|
|
||||||
|
| jour | fermeture | SOC max du jour |
|
||||||
|
|---|---|---:|
|
||||||
|
| 24/08 | 19:26 | 95 % |
|
||||||
|
| 25/08 | 19:29 | 95 % |
|
||||||
|
| 28/08 | 19:50 | 93 % |
|
||||||
|
| 29/08 | 18:47 | 94 % |
|
||||||
|
| 30/08 | 18:31 | 94 % |
|
||||||
|
| 31/08 | 20:06 (0 % d'excédent) | 95 % |
|
||||||
|
|
||||||
|
### Pourquoi la charge de journée, elle, est gratuite
|
||||||
|
|
||||||
|
La batterie **sature à 93-96 % tous les jours**. L'énergie détournée vers la
|
||||||
|
voiture entre 10 h et 18 h n'a donc coûté aucune réserve : le plateau était
|
||||||
|
atteint de toute façon, et la production au-delà était refusée (§ censure). Ce
|
||||||
|
n'est vrai que *tant que la batterie sature* — c'est le second terme de la
|
||||||
|
règle, et c'est lui qui bougera avec la saison.
|
||||||
|
|
||||||
|
### Le coût réel de la charge du soir
|
||||||
|
|
||||||
|
13,22 kWh pris à la réserve sur 8 jours, soit **1,65 kWh/jour**, rachetés au
|
||||||
|
réseau la nuit suivante — avec en plus le rendement aller-retour de la batterie
|
||||||
|
(~10 %) et un cycle de charge/décharge pour rien. Le tarif est le même (HC dans
|
||||||
|
les deux cas) : le surcoût est purement en pertes et en usure. Autrement dit,
|
||||||
|
charger le soir sur la batterie **revient à charger la voiture au réseau, en
|
||||||
|
plus cher et en abîmant le parc**.
|
||||||
|
|
||||||
|
### La deuxième question : combien garder pour la nuit ?
|
||||||
|
|
||||||
|
Elle est déjà résolue, et pas par un seuil. Le bilan publie
|
||||||
|
`evcs_balance_next_deficit_kwh` et `..._required_soc` — l'énergie de la
|
||||||
|
prochaine traversée déficitaire et le niveau à atteindre avant. Validation du
|
||||||
|
26/08 : point bas de la nuit prédit **à un point de SOC près sur douze heures**.
|
||||||
|
|
||||||
|
Mesure à 18 h, SOC requis pour franchir la nuit **sans réseau** :
|
||||||
|
|
||||||
|
| 24/08 | 25/08 | 26/08 | 27/08 | 28/08 | 29/08 | 30/08 | 31/08 |
|
||||||
|
|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||||
|
| 113,7 % | 119,4 % | 22,3 % ⚠️ | 114,3 % | 112,9 % | 112,3 % | 109,5 % | 117,9 % |
|
||||||
|
|
||||||
|
**Sept jours sur huit, même pleine, la batterie ne passe pas la nuit.** Déficit
|
||||||
|
structurel de 10 à 20 points, soit 1,5 à 3 kWh — c'est `storage_shortfall_kwh`,
|
||||||
|
le chiffre du dimensionnement, et il concorde avec les 11,8 kWh/jour jetés faute
|
||||||
|
de puits. L'import de nuit n'est donc pas évitable aujourd'hui ; ce qui est
|
||||||
|
évitable, c'est de l'**aggraver** en vidant la réserve le soir.
|
||||||
|
⚠️ Le 22,3 % du 26/08 est à vérifier (statut du bilan ce jour-là) avant d'être
|
||||||
|
cité.
|
||||||
|
|
||||||
|
### La règle qui en découle — deux termes, tous deux mesurés
|
||||||
|
|
||||||
|
```
|
||||||
|
charger le véhicule <=> (a) la batterie ne se décharge pas pour le nourrir
|
||||||
|
ET (b) elle atteindra quand même le niveau exigé par la
|
||||||
|
prochaine traversée déficitaire
|
||||||
|
```
|
||||||
|
|
||||||
|
- **(a)** est instantané, physique, déjà mesuré (`battery_power`), et se pilote
|
||||||
|
en asservissement : monter le courant tant que la batterie ne décharge pas,
|
||||||
|
redescendre dès qu'elle décharge. Ce n'est pas un seuil, c'est un servo.
|
||||||
|
- **(b)** est le bilan, déjà calculé toutes les 5 minutes
|
||||||
|
(`nextDeficit.reachable`). C'est lui qui portera la saison : quand les jours
|
||||||
|
raccourciront, la batterie cessera de saturer et (b) se fermera de lui-même,
|
||||||
|
de plus en plus tôt — sans qu'aucune constante n'ait à être touchée.
|
||||||
|
|
||||||
|
Ce couple remplace `batteryAssistSolarStartOk` (SOC ≥ 90), `soc_start_min`,
|
||||||
|
`midday_soc_min` et les planchers de profil : tous sont des mandataires de « la
|
||||||
|
production est refusée », justes à midi et faux au crépuscule.
|
||||||
|
|
||||||
|
### L'étude longue : rien à instrumenter
|
||||||
|
|
||||||
|
Constat utile : **l'historique HA suffit**. 2 s de résolution sur les puissances,
|
||||||
|
90 jours de rétention, et le bilan qui publie ses helpers toutes les 5 minutes
|
||||||
|
depuis le 24/08. La grandeur à suivre semaine après semaine est l'**heure de
|
||||||
|
fermeture de la frontière** et le **SOC max du jour** ; 8 jours ne montrent
|
||||||
|
aucune dérive saisonnière exploitable — il en faut plusieurs semaines, ce qui
|
||||||
|
est précisément l'objet de l'étude.
|
||||||
|
|
||||||
|
Bénéfice second, non cherché : les mêmes relevés donnent la courbe
|
||||||
|
énergie ↔ point de SOC par bande, c'est-à-dire le point (a) de l'ordre de
|
||||||
|
travail du 26/08, qui bloque tout câblage de la cible du matin.
|
||||||
|
|||||||
228
tools/etude-frontiere-excedent.mjs
Normal file
228
tools/etude-frontiere-excedent.mjs
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Étude de la frontière « excédent » pour la charge du véhicule.
|
||||||
|
//
|
||||||
|
// Question posée par le propriétaire (31/08/2026) : la voiture doit être un
|
||||||
|
// délestage de l'excédent PV, pas une charge prioritaire qui vide les
|
||||||
|
// batteries. La frontière se détecte à la PUISSANCE DE DÉCHARGE BATTERIE,
|
||||||
|
// pas à la production PV ni à un seuil de SOC.
|
||||||
|
//
|
||||||
|
// Ce script ne décide rien et n'écrit rien. Il intègre l'historique HA
|
||||||
|
// (résolution native ~2 s) et répartit l'énergie réellement livrée au
|
||||||
|
// véhicule entre trois origines physiques, instant par instant :
|
||||||
|
//
|
||||||
|
// batterie : min(P_evcs, décharge batterie) -> réserve entamée
|
||||||
|
// réseau : min(reste, import réseau) -> énergie payante
|
||||||
|
// excédent : le solde -> production qui aurait
|
||||||
|
// été refusée ou stockée
|
||||||
|
//
|
||||||
|
// Usage: node tools/etude-frontiere-excedent.mjs [debut] [fin]
|
||||||
|
// node tools/etude-frontiere-excedent.mjs 2026-08-24 2026-09-01
|
||||||
|
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
const HA_URL = process.env.HA_URL || 'http://10.10.10.17:8123';
|
||||||
|
|
||||||
|
function loadToken() {
|
||||||
|
if (process.env.HA_TOKEN) return process.env.HA_TOKEN;
|
||||||
|
const envPath = path.join(ROOT, '.env');
|
||||||
|
for (const line of fs.readFileSync(envPath, 'utf8').split('\n')) {
|
||||||
|
const m = line.match(/^\s*HA_TOKEN\s*=\s*(.+?)\s*$/);
|
||||||
|
if (m) return m[1].replace(/^["']|["']$/g, '');
|
||||||
|
}
|
||||||
|
console.error('HA_TOKEN absent de .env');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const TOKEN = loadToken();
|
||||||
|
|
||||||
|
const ENT = {
|
||||||
|
evcs: 'sensor.victron_evcs_ac_power',
|
||||||
|
batt: 'sensor.victron_battery_power',
|
||||||
|
pv: 'sensor.victron_pv_power_system',
|
||||||
|
conso:'sensor.victron_consumption_power_l1',
|
||||||
|
grid: 'sensor.victron_grid_power_l1',
|
||||||
|
soc: 'sensor.victron_battery_soc',
|
||||||
|
};
|
||||||
|
const KEYS = Object.keys(ENT);
|
||||||
|
|
||||||
|
async function fetchDay(dayStart, dayEnd) {
|
||||||
|
const url =
|
||||||
|
`${HA_URL}/api/history/period/${dayStart.toISOString()}` +
|
||||||
|
`?end_time=${encodeURIComponent(dayEnd.toISOString())}` +
|
||||||
|
`&filter_entity_id=${encodeURIComponent(Object.values(ENT).join(','))}` +
|
||||||
|
`&minimal_response`;
|
||||||
|
const res = await fetch(url, { headers: { Authorization: `Bearer ${TOKEN}` } });
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status} sur ${dayStart.toISOString()}`);
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fusionne les séries en un flux d'événements trié.
|
||||||
|
function toEvents(series) {
|
||||||
|
const byEntity = new Map();
|
||||||
|
for (const rows of series) {
|
||||||
|
if (!rows.length) continue;
|
||||||
|
byEntity.set(rows[0].entity_id, rows);
|
||||||
|
}
|
||||||
|
const ev = [];
|
||||||
|
for (const [key, id] of Object.entries(ENT)) {
|
||||||
|
for (const r of byEntity.get(id) || []) {
|
||||||
|
const v = Number(r.state);
|
||||||
|
if (!Number.isFinite(v)) continue;
|
||||||
|
ev.push({ t: Date.parse(r.last_changed || r.last_updated), key, v });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ev.sort((a, b) => a.t - b.t);
|
||||||
|
return ev;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_GAP_MS = 300e3; // au-delà, on considère une lacune et on n'intègre pas
|
||||||
|
|
||||||
|
const HOURS = 24;
|
||||||
|
function blankDay() {
|
||||||
|
return {
|
||||||
|
evcsKwh: 0, fromBattKwh: 0, fromGridKwh: 0, fromSurplusKwh: 0,
|
||||||
|
pvKwh: 0, consoKwh: 0, gridImportKwh: 0, battDischargeKwh: 0,
|
||||||
|
gapSec: 0,
|
||||||
|
hour: Array.from({ length: HOURS }, () => ({
|
||||||
|
evcsKwh: 0, fromBattKwh: 0, fromGridKwh: 0, fromSurplusKwh: 0,
|
||||||
|
})),
|
||||||
|
socMin: null, socMax: null,
|
||||||
|
lastSurplusMs: null, // dernier instant ou la voiture chargeait SANS decharger la batterie
|
||||||
|
firstSurplusMs: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const endArg = args[1] ? new Date(args[1]) : new Date();
|
||||||
|
const startArg = args[0] ? new Date(args[0]) : new Date(endArg.getTime() - 8 * 864e5);
|
||||||
|
|
||||||
|
const days = new Map(); // 'YYYY-MM-DD' (Paris) -> agrégats
|
||||||
|
|
||||||
|
const parisParts = (ms) => {
|
||||||
|
const d = new Date(ms);
|
||||||
|
const f = new Intl.DateTimeFormat('fr-CA', {
|
||||||
|
timeZone: 'Europe/Paris', year: 'numeric', month: '2-digit',
|
||||||
|
day: '2-digit', hour: '2-digit', hour12: false,
|
||||||
|
}).formatToParts(d);
|
||||||
|
const g = (t) => f.find((p) => p.type === t).value;
|
||||||
|
return { day: `${g('year')}-${g('month')}-${g('day')}`, hour: Number(g('hour')) % 24 };
|
||||||
|
};
|
||||||
|
|
||||||
|
let cursor = new Date(startArg);
|
||||||
|
const cur = {}; // dernières valeurs connues
|
||||||
|
let lastT = null;
|
||||||
|
|
||||||
|
while (cursor < endArg) {
|
||||||
|
const chunkEnd = new Date(Math.min(cursor.getTime() + 864e5, endArg.getTime()));
|
||||||
|
process.stderr.write(` lecture ${cursor.toISOString().slice(0, 10)}...\n`);
|
||||||
|
const ev = toEvents(await fetchDay(cursor, chunkEnd));
|
||||||
|
|
||||||
|
for (const e of ev) {
|
||||||
|
if (lastT !== null) {
|
||||||
|
const dtMs = e.t - lastT;
|
||||||
|
if (dtMs > 0) {
|
||||||
|
const { day, hour } = parisParts(lastT);
|
||||||
|
if (!days.has(day)) days.set(day, blankDay());
|
||||||
|
const D = days.get(day);
|
||||||
|
if (dtMs > MAX_GAP_MS) {
|
||||||
|
D.gapSec += dtMs / 1000;
|
||||||
|
} else if (KEYS.every((k) => cur[k] !== undefined)) {
|
||||||
|
const h = dtMs / 3.6e6; // heures
|
||||||
|
const evcs = Math.max(0, cur.evcs);
|
||||||
|
const battDis = Math.max(0, -cur.batt); // W déchargés
|
||||||
|
const gridImp = Math.max(0, cur.grid); // W importés
|
||||||
|
|
||||||
|
const fromBatt = Math.min(evcs, battDis);
|
||||||
|
const fromGrid = Math.min(evcs - fromBatt, gridImp);
|
||||||
|
const fromSurp = Math.max(0, evcs - fromBatt - fromGrid);
|
||||||
|
|
||||||
|
D.evcsKwh += evcs * h / 1000;
|
||||||
|
D.fromBattKwh += fromBatt * h / 1000;
|
||||||
|
D.fromGridKwh += fromGrid * h / 1000;
|
||||||
|
D.fromSurplusKwh += fromSurp * h / 1000;
|
||||||
|
D.pvKwh += Math.max(0, cur.pv) * h / 1000;
|
||||||
|
D.consoKwh += Math.max(0, cur.conso) * h / 1000;
|
||||||
|
D.gridImportKwh += gridImp * h / 1000;
|
||||||
|
D.battDischargeKwh += battDis * h / 1000;
|
||||||
|
|
||||||
|
if (evcs > 200 && fromSurp > 0.6 * evcs) {
|
||||||
|
if (D.firstSurplusMs === null) D.firstSurplusMs = lastT;
|
||||||
|
D.lastSurplusMs = lastT;
|
||||||
|
}
|
||||||
|
|
||||||
|
const H = D.hour[hour];
|
||||||
|
H.evcsKwh += evcs * h / 1000;
|
||||||
|
H.fromBattKwh += fromBatt * h / 1000;
|
||||||
|
H.fromGridKwh += fromGrid * h / 1000;
|
||||||
|
H.fromSurplusKwh += fromSurp * h / 1000;
|
||||||
|
}
|
||||||
|
if (cur.soc !== undefined) {
|
||||||
|
D.socMin = D.socMin === null ? cur.soc : Math.min(D.socMin, cur.soc);
|
||||||
|
D.socMax = D.socMax === null ? cur.soc : Math.max(D.socMax, cur.soc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cur[e.key] = e.v;
|
||||||
|
lastT = e.t;
|
||||||
|
}
|
||||||
|
cursor = chunkEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
const r1 = (x) => x.toFixed(1);
|
||||||
|
const r2 = (x) => x.toFixed(2);
|
||||||
|
|
||||||
|
console.log('\n=== ORIGINE DE L\'ENERGIE LIVREE AU VEHICULE (kWh/jour) ===\n');
|
||||||
|
console.log('jour EVCS excedent batterie reseau | PV conso import SOC');
|
||||||
|
let tot = { e: 0, s: 0, b: 0, g: 0 };
|
||||||
|
const sorted = [...days.keys()].sort();
|
||||||
|
for (const d of sorted) {
|
||||||
|
const D = days.get(d);
|
||||||
|
if (D.gapSec > 3600) console.log(` (lacune historique ${Math.round(D.gapSec / 60)} min le ${d})`);
|
||||||
|
tot.e += D.evcsKwh; tot.s += D.fromSurplusKwh; tot.b += D.fromBattKwh; tot.g += D.fromGridKwh;
|
||||||
|
console.log(
|
||||||
|
`${d} ${r2(D.evcsKwh).padStart(6)} ${r2(D.fromSurplusKwh).padStart(7)} ` +
|
||||||
|
`${r2(D.fromBattKwh).padStart(7)} ${r2(D.fromGridKwh).padStart(7)} | ` +
|
||||||
|
`${r1(D.pvKwh).padStart(5)} ${r1(D.consoKwh).padStart(6)} ${r1(D.gridImportKwh).padStart(6)} ` +
|
||||||
|
`${D.socMin ?? '?'}-${D.socMax ?? '?'}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log(`\nTOTAL ${r2(tot.e).padStart(6)} ${r2(tot.s).padStart(7)} ${r2(tot.b).padStart(7)} ${r2(tot.g).padStart(7)}`);
|
||||||
|
if (tot.e > 0) {
|
||||||
|
console.log(`part 100% ${(100 * tot.s / tot.e).toFixed(0).padStart(6)}% ` +
|
||||||
|
`${(100 * tot.b / tot.e).toFixed(0).padStart(6)}% ${(100 * tot.g / tot.e).toFixed(0).padStart(6)}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hhmm = (ms) => ms === null ? ' -- ' : new Date(ms).toLocaleTimeString('fr-FR',
|
||||||
|
{ timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit' });
|
||||||
|
|
||||||
|
console.log('\n=== FRONTIERE DE L\'EXCEDENT, JOUR PAR JOUR ===\n');
|
||||||
|
console.log('jour ouverture fermeture SOC max part excedent 16h 17h 18h 19h 20h');
|
||||||
|
for (const d of sorted) {
|
||||||
|
const D = days.get(d);
|
||||||
|
const sh = (h) => {
|
||||||
|
const H = D.hour[h];
|
||||||
|
return H.evcsKwh < 0.05 ? ' . ' : `${(100 * H.fromSurplusKwh / H.evcsKwh).toFixed(0).padStart(3)}%`;
|
||||||
|
};
|
||||||
|
console.log(
|
||||||
|
`${d} ${hhmm(D.firstSurplusMs)} ${hhmm(D.lastSurplusMs)} ` +
|
||||||
|
`${String(D.socMax ?? '?').padStart(3)}% ` +
|
||||||
|
`${sh(16)} ${sh(17)} ${sh(18)} ${sh(19)} ${sh(20)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n=== PAR HEURE DE LA JOURNEE, cumul sur la periode (kWh) ===\n');
|
||||||
|
console.log('h EVCS excedent batterie reseau part excedent');
|
||||||
|
for (let h = 0; h < HOURS; h++) {
|
||||||
|
let e = 0, s = 0, b = 0, g = 0;
|
||||||
|
for (const d of sorted) {
|
||||||
|
const H = days.get(d).hour[h];
|
||||||
|
e += H.evcsKwh; s += H.fromSurplusKwh; b += H.fromBattKwh; g += H.fromGridKwh;
|
||||||
|
}
|
||||||
|
if (e < 0.01) continue;
|
||||||
|
console.log(
|
||||||
|
`${String(h).padStart(2)}h ${r2(e).padStart(7)} ${r2(s).padStart(7)} ${r2(b).padStart(7)} ` +
|
||||||
|
`${r2(g).padStart(7)} ${(100 * s / e).toFixed(0).padStart(3)}%`
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user