Chauffe-eau en HC de nuit les jours faibles : decision quotidienne du bilan, workflow ECS sous controle du depot (VRM, prevision du jour), energie 24 h
This commit is contained in:
14
tools/chauffe-eau.mjs
Normal file
14
tools/chauffe-eau.mjs
Normal file
@@ -0,0 +1,14 @@
|
||||
// Energie du chauffe-eau sur 24 h (07/10/2026), pour la regle « jour faible :
|
||||
// eau chaude en HC de nuit » du workflow Water Heater Advisor ECS 6 Nodes :
|
||||
// moins de 1,5 kWh en 24 h -> il chauffe quand meme (securite eau chaude).
|
||||
// Somme des HAUSSES du compteur cumule : une remise a zero ou une lecture
|
||||
// indisponible ne cree ni energie negative ni saut.
|
||||
export const COMPTEUR_CHAUFFE_EAU = 'sensor.tt_chauffe_eau_pleumeur_energie_totale';
|
||||
|
||||
export function energieSur24h(serie) {
|
||||
const vals = (serie || []).map((e) => Number.parseFloat(e.state)).filter(Number.isFinite);
|
||||
if (!vals.length) return null; // compteur illisible sur 24 h : on ne dit rien
|
||||
let kwh = 0;
|
||||
for (let i = 1; i < vals.length; i++) if (vals[i] > vals[i - 1]) kwh += vals[i] - vals[i - 1];
|
||||
return Math.round(kwh * 100) / 100;
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import { creerSchemaKm, ingererReleves, ingererNiveauSaisi, ingererJeep } from '
|
||||
import { texteRapport, envoyer, donneesNuit, texteDepuis } from './rapport-nuit.mjs';
|
||||
import { diagnosticPv, etatCellules, productionJour, chargeSolaire, bilanNuit, etatReseau, importJour, minuitLocal, COMPTEURS_IMPORT, COMPTEUR_EDF, consoJour, COMPTEURS_CONSO, projectionSoir, previsionNuit, vrmVersCourbes, nuitMaisonPrevue } from './diagnostics-agent.mjs';
|
||||
import { transitionsDansSerie } from './allers-retours.mjs';
|
||||
import { energieSur24h, COMPTEUR_CHAUFFE_EAU } from './chauffe-eau.mjs';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const ONCE = process.argv.includes('--once');
|
||||
@@ -2064,6 +2065,25 @@ async function valeursAMinuit() {
|
||||
return valeurs;
|
||||
}
|
||||
|
||||
// Chauffe-eau sur 24 h (07/10/2026) : securite eau chaude de la regle « jour
|
||||
// faible » du workflow ECS. Illisible -> « indisponible » : le workflow ne
|
||||
// restreint alors rien (jamais un ballon froid par panne de capteur).
|
||||
async function publierChauffeEau24h() {
|
||||
const fin = new Date(), debut = new Date(fin.getTime() - 24 * 3600e3);
|
||||
const r = await fetch(
|
||||
`${HA_URL}/api/history/period/${debut.toISOString()}?end_time=${encodeURIComponent(fin.toISOString())}` +
|
||||
`&filter_entity_id=${COMPTEUR_CHAUFFE_EAU}&minimal_response&no_attributes`,
|
||||
{ headers: { Authorization: `Bearer ${HA_TOKEN}` }, signal: AbortSignal.timeout(15000) },
|
||||
);
|
||||
if (!r.ok) throw new Error(`historique HA ${r.status}`);
|
||||
const kwh = energieSur24h((await r.json())[0]);
|
||||
await publierCapteur('sensor.evcs_chauffe_eau_24h_kwh', kwh === null ? 'indisponible' : kwh, {
|
||||
friendly_name: 'EVCS — chauffe-eau sur 24 h', unit_of_measurement: 'kWh', icon: 'mdi:water-boiler',
|
||||
compteur: COMPTEUR_CHAUFFE_EAU, calcule_a: fin.toISOString(),
|
||||
});
|
||||
return 1;
|
||||
}
|
||||
|
||||
async function publierImportJour(st) {
|
||||
const minuit = await valeursAMinuit();
|
||||
const courant = {};
|
||||
@@ -2171,6 +2191,7 @@ async function publierNuitMaison(client) {
|
||||
return 1;
|
||||
}
|
||||
let bilanNuitPublie = false; // publie des le premier cycle, puis toutes les 5 min
|
||||
let chauffeEauPublie = false; // chauffe-eau sur 24 h (07/10) : premier cycle puis toutes les 5 min
|
||||
async function publierBilanNuit(client) {
|
||||
if (DRY || !client) return 0;
|
||||
const d = await donneesNuit(client);
|
||||
@@ -2282,6 +2303,9 @@ async function cycle(client) {
|
||||
nuitMaison: (DRY || (nuitMaisonPubliee && new Date().getMinutes() % 5 !== 2)) ? 0 : await publierNuitMaison(client).then((n) => { nuitMaisonPubliee = true; return n; }).catch((e) => {
|
||||
console.error(`nuit de la maison : ${e.message}`); return 0;
|
||||
}),
|
||||
chauffeEau24h: (DRY || (chauffeEauPublie && new Date().getMinutes() % 5 !== 4)) ? 0 : await publierChauffeEau24h().then((n) => { chauffeEauPublie = true; return n; }).catch((e) => {
|
||||
console.error(`chauffe-eau 24 h : ${e.message}`); return 0;
|
||||
}),
|
||||
bilanNuit: (DRY || (bilanNuitPublie && new Date().getMinutes() % 5 !== 1)) ? 0 : await publierBilanNuit(client).then((n) => { bilanNuitPublie = true; return n; }).catch((e) => {
|
||||
console.error(`bilan de nuit : ${e.message}`); return 0;
|
||||
}),
|
||||
|
||||
@@ -61,6 +61,22 @@ export const WORKFLOWS = [
|
||||
{ file: 'HA_SNAPSHOT_FORECAST.json.tpl', node: 'HA Snapshot Forecast', kind: 'template' },
|
||||
],
|
||||
},
|
||||
{
|
||||
// Chauffe-eau (07/10/2026) : seul workflow ECS actif (V2.3 desactive le 07/10).
|
||||
id: 'water-heater',
|
||||
workflowFile: 'workflows/water-heater-ecs-v1.json',
|
||||
srcDir: 'src/water-heater',
|
||||
nodes: [
|
||||
{ file: 'NORMALIZE.js', node: 'ECS Normalize Inputs', kind: 'jsCode' },
|
||||
{ file: 'OBSERVATION.js', node: 'ECS EVCS & Water Heater Observation', kind: 'jsCode' },
|
||||
{ file: 'ENERGY_MODEL.js', node: 'ECS Energy Model & Early Boost Signals', kind: 'jsCode' },
|
||||
{ file: 'BASE_DECISION.js', node: 'ECS Base Advisor Decision', kind: 'jsCode' },
|
||||
{ file: 'ARBITRATION.js', node: 'ECS Final Arbitration & Protection', kind: 'jsCode' },
|
||||
{ file: 'OUTPUT_BUILDER.js', node: 'ECS Output Builder', kind: 'jsCode' },
|
||||
{ file: 'BUILD_ACTIONS.js', node: 'BUILD ECS HA Actions1', kind: 'jsCode' },
|
||||
{ file: 'HA_SNAPSHOT_TEMPLATE.json.tpl', node: 'HA Snapshot Water Heater Advisor V2', kind: 'template' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Rétro-compatibilité (outils historiques mono-workflow)
|
||||
|
||||
@@ -1,47 +1,23 @@
|
||||
#!/usr/bin/env node
|
||||
// Usage: node tools/n8n-pull.mjs [workflowId] [outputFile]
|
||||
// GET un workflow n8n et l'écrit en JSON (pretty) dans un fichier local.
|
||||
// Clé API lue depuis ~/.config/opencode/opencode.json (mcp.n8n-mcp.environment).
|
||||
|
||||
// Usage: node tools/n8n-pull.mjs <workflowId> <fichierSortie>
|
||||
// GET d'un workflow n8n vers un fichier local (mise sous controle du depot).
|
||||
// Meme configuration d'API que n8n-push.mjs. N'ecrit rien sur le serveur.
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
const WORKFLOW_ID = process.argv[2] || 'ATswvGfG4KJUls0K';
|
||||
const OUT = process.argv[3] || `workflows/pull-${WORKFLOW_ID}.json`;
|
||||
|
||||
const [ID, OUT] = process.argv.slice(2);
|
||||
if (!ID || !OUT) { console.error('Usage: node tools/n8n-pull.mjs <workflowId> <fichierSortie>'); process.exit(1); }
|
||||
function loadApiConfig() {
|
||||
if (process.env.N8N_API_KEY) {
|
||||
return {
|
||||
url: process.env.N8N_URL || 'https://n8n.airtria.pro',
|
||||
key: process.env.N8N_API_KEY
|
||||
};
|
||||
}
|
||||
const cfgPath = path.join(os.homedir(), '.config/opencode/opencode.json');
|
||||
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
||||
if (process.env.N8N_API_KEY) return { url: process.env.N8N_URL || 'https://n8n.airtria.pro', key: process.env.N8N_API_KEY };
|
||||
const cfg = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.config/opencode/opencode.json'), 'utf8'));
|
||||
const env = cfg?.mcp?.['n8n-mcp']?.environment || {};
|
||||
if (!env.N8N_API_KEY) {
|
||||
console.error('Clé API n8n introuvable (env N8N_API_KEY ou opencode.json)');
|
||||
process.exit(1);
|
||||
}
|
||||
return {
|
||||
url: (env.N8N_API_URL || 'https://n8n.airtria.pro/api/v1').replace(/\/api\/v1\/?$/, ''),
|
||||
key: env.N8N_API_KEY
|
||||
};
|
||||
if (!env.N8N_API_KEY) { console.error('Clé API n8n introuvable'); process.exit(1); }
|
||||
return { url: (env.N8N_API_URL || 'https://n8n.airtria.pro/api/v1').replace(/\/api\/v1\/?$/, ''), key: env.N8N_API_KEY };
|
||||
}
|
||||
|
||||
const { url, key } = loadApiConfig();
|
||||
|
||||
const res = await fetch(`${url}/api/v1/workflows/${WORKFLOW_ID}`, {
|
||||
headers: { 'X-N8N-API-KEY': key }
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error(`GET échoué: ${res.status} ${res.statusText}`);
|
||||
console.error(await res.text());
|
||||
process.exit(1);
|
||||
}
|
||||
const wf = await res.json();
|
||||
fs.mkdirSync(path.dirname(OUT), { recursive: true });
|
||||
const r = await fetch(`${url}/api/v1/workflows/${ID}`, { headers: { 'X-N8N-API-KEY': key } });
|
||||
if (!r.ok) { console.error(`GET ${r.status}`); process.exit(1); }
|
||||
const wf = await r.json();
|
||||
fs.writeFileSync(OUT, JSON.stringify(wf, null, 2) + '\n');
|
||||
console.log(`OK: "${wf.name}" (${wf.nodes?.length} nodes, active=${wf.active}) -> ${OUT}`);
|
||||
console.log(`updatedAt: ${wf.updatedAt}`);
|
||||
console.log(`${OUT} ← "${wf.name}" (${wf.nodes.length} nodes, active=${wf.active})`);
|
||||
|
||||
@@ -110,6 +110,7 @@ const isEvcsWorkflow = path.basename(INPUT) === 'evcs-surplus-controller-v2.6.js
|
||||
const isAdvisorWorkflow = path.basename(INPUT) === 'evcs-advisor-shadow-v1.json';
|
||||
const isHysteresisWorkflow = path.basename(INPUT) === 'victron-grid-hysteresis-v1.json';
|
||||
const isAcmWorkflow = path.basename(INPUT) === 'victron-auto-charge-manager-v1.2.json';
|
||||
const isWaterHeaterWorkflow = path.basename(INPUT) === 'water-heater-ecs-v1.json';
|
||||
const isBalanceWorkflow = path.basename(INPUT) === 'evcs-energy-balance-v1.json'
|
||||
|| path.basename(INPUT) === 'evcs-energy-profiles-daily-v1.json'
|
||||
|| path.basename(INPUT) === 'evcs-solar-forecast-v1.json';
|
||||
@@ -120,6 +121,7 @@ const testSuite =
|
||||
isHysteresisWorkflow && fs.existsSync('tests/golden-hysteresis/run.mjs') ? 'tests/golden-hysteresis/run.mjs' :
|
||||
isAcmWorkflow && fs.existsSync('tests/golden-acm/run.mjs') ? 'tests/golden-acm/run.mjs' :
|
||||
isBalanceWorkflow && fs.existsSync('tests/energy-balance/run.mjs') ? 'tests/energy-balance/run.mjs' :
|
||||
isWaterHeaterWorkflow && fs.existsSync('tests/water-heater/run.mjs') ? 'tests/water-heater/run.mjs' :
|
||||
null;
|
||||
if (testSuite) {
|
||||
if (skipGolden) {
|
||||
|
||||
Reference in New Issue
Block a user