La consigne de puissance reseau appartient au controleur

Fermer le contacteur ne suffit pas : au-dessus du SOC minimal, l ESS en
autoconsommation ne preleve que sa consigne et laisse la batterie porter
maison et voiture. Nuit du 01/09 : charge HC demarree a 01:25, arretee a
01:26 par le garde batterie, contacteur ferme et inerte jusqu a 02:00.

La consigne a 4200 W du proprietaire n etait pas decorative : c est elle
qui faisait fonctionner la prise de relais nocturne depuis le 07/08. La
retirer le 31/08 a supprime la substitution de journee et casse la nuit.

Le controleur ecrit desormais la consigne lui-meme, a partir du besoin
mesure, seulement pendant une session reseau deliberee, avec une bande
asymetrique : trop basse se corrige tout de suite, trop haute ne coute
rien. Deux ecritures en 25 minutes au lieu d une toutes les 30 s.

Golden 51 -> 56. Verifie en reel : batterie a 0 A, voiture a 3500 W.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
This commit is contained in:
Tallagrand Thierry
2026-09-01 02:14:57 +02:00
parent 92f07fe885
commit a57ff19ead
8 changed files with 297 additions and 4 deletions

View File

@@ -124,6 +124,17 @@ pushAction(
context.sourceMode === 'grid_hc_allowed' || context.sourceMode === 'grid_forced'
);
// =====================================================
// CONSIGNE DE PUISSANCE RESEAU
// Fermer le contacteur ne suffit pas : sans consigne, l'ESS laisse la
// batterie porter la maison et la voiture (nuit du 01/09/2026).
// =====================================================
pushAction(
'grid_power_setpoint',
calls.grid_power_setpoint,
!!calls.grid_power_setpoint?.execute
);
// =====================================================
// AUTO START OFF SI BESOIN
// =====================================================

View File

@@ -1,3 +1,10 @@
// Bornes de la consigne de puissance reseau. Le maximum est celui declare par
// l'entite elle-meme (number.airtria_mpii_v1_ac_power_setpoint_l1, max 10000 W),
// pas un chiffre choisi ici. La bande morte evite de reecrire la meme valeur
// toutes les 30 s sans empecher la correction d'une derive externe.
const GRID_SETPOINT_MAX_W = 10000;
const GRID_SETPOINT_DEADBAND_W = 100;
function round1(v) {
return Math.round(v * 10) / 10;
}
@@ -309,6 +316,67 @@ reason.push(`SOC=${round1(parsed.soc)}%`);
reason.push(`Batt=${round1(parsed.batteryCurrentA)}A`);
reason.push(`PV=${Math.round(parsed.pvPowerW)}W`);
reason.push(`PV10=${Math.round(parsed.pvPowerAvg10mW)}W`);
// =====================================================
// CONSIGNE DE PUISSANCE RESEAU (01/09/2026)
// =====================================================
// Fermer le contacteur ne suffit PAS. Au-dessus du SOC minimal, l'ESS en
// autoconsommation ne preleve que `ac_power_setpoint` et laisse la batterie
// porter tout le reste : nuit du 01/09, contacteur ferme, consigne 50 W,
// batterie a -803 W pour 761 W de maison, voiture arretee par le garde
// "batterie trop sollicitee". La consigne est donc le levier qui rend une
// session reseau REELLE, et il doit appartenir au controleur, pas rester un
// reglage manuel permanent (celui-ci provoquait la substitution du solaire
// par le reseau en journee, cf. HANDOFF 27/28).
//
// La valeur n'est pas une constante : c'est le besoin mesure du moment, la
// maison telle qu'elle consomme plus la voiture au courant que le controleur
// s'autorise. Le plafond joue le role de marge : quand la voiture n'est pas a
// son plafond, la consigne est deja genereuse, et l'exces ne peut pas partir
// dans la batterie (DVCC tenue a 2 A par l'Auto Charge Manager pendant un
// takeover). Hors session, retour a la valeur de repos.
const gridSessionActive =
parsed.acInAvailable &&
(context.sourceMode === 'grid_forced' || context.sourceMode === 'grid_hc_allowed');
const gridSetpointNeedW = gridSessionActive
? clamp(
Math.round(derived.houseNonEvcsW + derived.physicalCapA * derived.acVoltageRef),
params.gridSetpointIdleW,
GRID_SETPOINT_MAX_W
)
: params.gridSetpointIdleW;
// La bande est ASYMETRIQUE, et c'est le point. Une consigne trop BASSE fait
// porter la difference par la batterie : c'est le defaut de la nuit du 01/09,
// il se corrige immediatement. Une consigne trop HAUTE ne coute rien : l'ESS
// ne preleve que ce qu'il peut placer, et le courant de charge de la batterie
// est tenu par ailleurs (DVCC, Auto Charge Manager). Deux leviers, deux roles
// -- la consigne dit ce que le reseau a le DROIT de servir, DVCC dit ce que la
// batterie a le droit de prendre. Ne pas confondre les deux.
//
// On ecrit donc avec une marge d'un pas de courant EVCS (grandeur du systeme,
// pas une constante inventee) pour que le bruit de la maison ne rende pas la
// consigne insuffisante au cycle suivant, et on ne redescend que si elle est
// devenue franchement inutile. Sans cela la boucle 30 s reecrivait a chaque
// cycle en poursuivant le bruit.
const gridSetpointSlackW = gridSessionActive
? Math.max(GRID_SETPOINT_DEADBAND_W, Math.round(params.currentStepUpA * derived.acVoltageRef))
: 0;
const gridSetpointTargetW = clamp(
gridSetpointNeedW + gridSetpointSlackW,
0,
GRID_SETPOINT_MAX_W
);
const gridSetpointCurrentW = Number.isFinite(parsed.gridSetpointW)
? parsed.gridSetpointW
: null;
const gridSetpointNeedsWrite =
gridSetpointCurrentW === null ||
gridSetpointCurrentW < gridSetpointNeedW ||
gridSetpointCurrentW > gridSetpointNeedW + 2 * gridSetpointSlackW + GRID_SETPOINT_DEADBAND_W;
reason.push(`Grid=${parsed.gridAvailable ? 'on' : 'off'} (${Math.round(parsed.gridPowerW)}W)`);
// Trace de la fenetre d'etablissement : sans elle, une lecture du logbook
// pendant le transitoire donne l'impression que le systeme decide n'importe
@@ -316,6 +384,9 @@ reason.push(`Grid=${parsed.gridAvailable ? 'on' : 'off'} (${Math.round(parsed.gr
if (derived.gridSettling) {
reason.push(`ETABLISSEMENT reseau ${Math.round(parsed.gridTransitionAgeSec)}s/${params.gridSettleSeconds}s (mesures PV/batterie non fiables)`);
}
if (gridSessionActive) {
reason.push(`Consigne reseau=${gridSetpointTargetW}W`);
}
reason.push(`Cap=${derived.physicalCapA}A`);
reason.push(`Ref=${round1(derived.currentReferenceA)}A`);
reason.push(`Target=${round1(desiredCurrentA)}A`);
@@ -1001,6 +1072,16 @@ return [{
}
},
grid_power_setpoint: {
execute: gridSetpointNeedsWrite,
domain: 'number',
service: 'set_value',
data: {
entity_id: 'number.gx_device_ac_power_setpoint',
value: gridSetpointTargetW
}
},
evcs_auto_start_off: {
execute: autoStartAction === 'turn_off',
domain: 'switch',

View File

@@ -17,6 +17,8 @@
"acin1_last_changed_ts": as_timestamp(states.sensor.victron_ac_in1_available.last_changed, 0) if states.sensor.victron_ac_in1_available is not none else 0,
"grid_switch_last_changed_ts": as_timestamp(states.switch.edf_general_ok_switch.last_changed, 0) if states.switch.edf_general_ok_switch is not none else 0,
"grid_settle_seconds": states("input_number.evcs_grid_settle_seconds"),
"grid_setpoint_w": states("number.gx_device_ac_power_setpoint"),
"grid_setpoint_idle_w": states("input_number.evcs_grid_setpoint_idle_w"),
"today_remaining_kwh": states("sensor.energy_production_today_remaining"),
"tomorrow_kwh": states("sensor.energy_production_tomorrow"),

View File

@@ -245,6 +245,13 @@ const parsed = {
gridIsOn: low(input.grid_state || 'off') === 'on',
acInAvailable: num(input.victron_acin1_available ?? input.ac_in1_available, 0) === 1,
// Consigne de puissance réseau de l'ESS, lue telle qu'elle est appliquée.
// C'est ELLE qui décide si un contacteur fermé sert à quelque chose : au-dessus
// du SOC minimal, l'ESS en autoconsommation ne prélève que cette puissance et
// laisse la batterie porter le reste. Nuit du 01/09 : contacteur fermé,
// consigne 50 W, batterie à -803 W pour 761 W de maison.
gridSetpointW: num(input.grid_setpoint_w, NaN),
gridAvailable:
(low(input.grid_state || 'off') === 'on') ||
(num(input.victron_acin1_available ?? input.ac_in1_available, 0) === 1),
@@ -419,6 +426,10 @@ const params = {
// input_number.evcs_grid_settle_seconds ; absent => 180.
gridSettleSeconds: clamp(num(input.grid_settle_seconds, 180), 0, 900),
// Consigne réseau au repos. 50 W est le défaut Victron (légèrement positif
// pour ne jamais injecter), pas une valeur choisie ici.
gridSetpointIdleW: clamp(num(input.grid_setpoint_idle_w, 50), 0, 500),
probeEnabled: low(input.probe_enabled || 'off') === 'on',
probeMinutes: Math.max(1, num(input.probe_minutes, 3)),
probeRetryMinutes: Math.max(5, num(input.probe_retry_minutes, 45)),