Retour d'expérience sur intégration Enphase par Modbus TCP

J’ouvre ce sujet afin de partager et d’échanger autour de l’intégration de la passerelle Enphase au GX par communication Modbus TCP.

Il y a quelques semaines Guilhem (@Guizmon) annonçait avoir rencontré quelques problèmes en réalisant cette intégration (voir Mulitplus Modbus + enphase = production solaire bloqué à 1kW). Finalement, ce n’était qu’une question de paramètres mal réglés suite à de multiples tests.

Enphase a produit une note technique à ce sujet, datant de janvier 2025, dans laquelle ils disent qu’il faut contacter le support EMEA (support_emea@enphaseenergy.com) pour demander une mise à jour du firmware. En réalité, il est probable que la passerelle soit déjà avec un firmware suffisamment récent car la fonctionnalité est disponible depuis la version 7.6.168 (actuellement 8.3.5167). Ce qu’ils ne disent pas et qui m’a fait perdre 2 semaines, c’est qu’après avoir demandé cette mise à jour probablement inutile, il faut contacter le support API (api@enphaseenergy.com) pour demander l’activation de la fonctionnalité. De plus, il ne faut pas transférer un mail venant du support EMEA vers le support API car il reviendra chez EMEA. Tout n’est pas facile avec Enphase.

Une fois la fonctionnalité activée, le GX est capable de trouver la passerelle via Modbus TCP comme décrit dans la note technique.

La régulation de production PV a été introduite en juin 2025 avec la version 3.60 du Venus OS (GX) qui supporte désormais la SunSpec 123 et 704.

J’ai réalisé des tests de régulation de la puissance au départ du GX. Le résultat ne fut pas très concluant. Ça fonctionne mais ce n’est pas du tout réactif. Pour mieux comprendre, j’ai analysé la communication entre le GX et la passerelle. J’ai découvert que la passerelle prend énormément de temps à traiter une demande de modification (environ 30s) et met en attente les demandes en lecture. Ceci signifie que le GX donne l’instruction à la passerelle Enphase de produire plus ou moins afin de réguler l’énergie produite pour se conformer aux paramètres, tels que la consigne réseau ou la limitation de puissance exportée, alors que la passerelle ne les applique que 30s plus tard. Avec une météo variable ou des consommations variables, il n’est pas possible de maîtriser l’exportation d’énergie avec cette technologie. Ceci pourrait changer à l’avenir si Enphase améliore son serveur Modbus afin de traiter immédiatement les demandes de modification.

Les compteurs d’énergie supportés par Victron ont des délais de rafraîchissement allant de 0,1 à 2s (cfr Selection Guide - Energy Meters). On peut facilement comprendre que le GX ne peut pas faire correctement son job avec une mesure qui est rafraîchie toutes les 30s. De mémoire, le GX n’interroge la passerelle que toutes les 10s. Donc, si le traitement des demandes de modification devenait instantané, le délais de rafraîchissement tomberait à 10s ce qui est mieux mais ça ne vaut pas un véritable compteur.

Durant mes tests, j’ai également mis le port de communication Modbus (TCP 502) sur la passerelle Enphase plusieurs fois en erreur totale n’acceptant plus aucune nouvelle communication même après un redémarrage de la passerelle. Je suppose qu’un client (le GX ou le module Node-RED) envoie un TCP ACK pour maintenir la session ouverte et bloque le port de la passerelle. Je n’ai pas suffisamment cherché à identifier la source du problème mais la conséquence a été qu’un matin, le GX n’était plus capable de réguler la production. L’analyse réseau a montré un échange en boucle de TCP ACK entre le client (le GX ou le module Node-RED) et la passerelle empêchant la passerelle d’accepter de nouvelles demandes d’ouverture de session (TCP SYN). Il y a dès lors un défaut de traitement des communications TCP/IP qui devrait être corrigé. C’est bon à savoir si subitement plus rien ne fonctionne. On peut espérer qu’Enphase apporte les corrections nécessaires dans les prochaines mises à jour. Encore, faut-il leur signifier le problème ce qui signifie correctement identifier ce qui produit le problème.

Vu ces problèmes, j’ai abandonné l’idée de réguler ma production avec le GX pour la réaliser en Node-RED grâce au module node-red-contrib-modbus et les références SunSpec. Lorsque je ne veux pas ou ne peux pas exporter, je ralenti progressivement la production pour la caler au plus proche de la consommation vers un SoC à 97%. Ainsi, je parviens à maintenir le SoC et à maintenir l’exportation ou l’importation conformément à la consigne réseau.

Voici la base du code Node-RED. Attention, les adresses Modbus pourraient évoluer avec les mises à jour d’Enphase. A ce moment, il faudra réaliser un processus de découverte pour obtenir les bonnes adresses des SunSpec 702 et 704.

[{"id":"cc1ff62cbeafe5d7","type":"modbus-read","z":"77403a7b4cd9572e","name":"SunSpec 704","topic":"SunSpec 704","showStatusActivities":false,"logIOActivities":false,"showErrors":true,"showWarnings":true,"unitid":"126","dataType":"HoldingRegister","adr":"40296","quantity":"67","rate":"1","rateUnit":"m","delayOnStart":false,"startDelayTime":"92","server":"3df6c636aa6cabec","useIOFile":false,"ioFile":"","useIOForPayload":false,"emptyMsgOnFail":true,"x":130,"y":140,"wires":[[],["7e8c688972c35b7b"]]},{"id":"7ac9b7ddb4f0cdce","type":"modbus-read","z":"77403a7b4cd9572e","name":"SunSpec 702","topic":"SunSpec 702","showStatusActivities":false,"logIOActivities":false,"showErrors":true,"showWarnings":true,"unitid":"126","dataType":"HoldingRegister","adr":"40225","quantity":"52","rate":"1","rateUnit":"m","delayOnStart":false,"startDelayTime":"91","server":"3df6c636aa6cabec","useIOFile":false,"ioFile":"","useIOForPayload":false,"emptyMsgOnFail":true,"x":130,"y":100,"wires":[[],["22a286cb05e5bddf"]]},{"id":"7e8c688972c35b7b","type":"function","z":"77403a7b4cd9572e","name":"Set flow variable sunspec_704","func":"let sunspec = { data: msg.payload.data, buffer: msg.payload.buffer, datetime: Date.now() };\n\nsunspec.valid = sunspec.data[0] == 704 && sunspec.data[1] == 65 && sunspec.data.length == 67;\n\nlet msg1 = null;\n\nif (sunspec.valid) {\n    node.status({ fill: \"grey\", shape: \"dot\", text: 'Limit ' + (sunspec.data[14] == 1 ? sunspec.data[15] + '%' : 'disabled')});\n    \n    sunspec.w_max_lim_ena = sunspec.data[14];\n    sunspec.w_max_lim_pct = sunspec.data[15];\n    sunspec.w_max_lim_rvrt_ena = sunspec.data[17];\n    sunspec.w_max_lim_rvrt_pct = sunspec.data[16];\n    sunspec.w_max_lim_rvrt_tms = sunspec.data[18];\n\n    let prod_limit = '-';\n    if (sunspec.w_max_lim_ena) {\n        let ss702 = flow.get('sunspec_702');\n        if (typeof ss702 === 'object' && ss702.datetime > Date.now() - 180000) {\n            prod_limit = Math.round(ss702.w_max*sunspec.w_max_lim_pct/100);\n        } else {\n            prod_limit = sunspec.w_max_lim_pct + '%'\n        }\n    }\n    \n    msg1 = { topic: 'Victron/Settings/Prod Limit', payload: prod_limit };\n} else {\n    node.status({ fill: 'red', shape: 'dot', text: 'Invalid data' });\n}\n\nflow.set('sunspec_704', sunspec);\n\nreturn msg1;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":370,"y":140,"wires":[[]]},{"id":"22a286cb05e5bddf","type":"function","z":"77403a7b4cd9572e","name":"Set flow variable sunspec_702","func":"let sunspec = { data: msg.payload.data, buffer: msg.payload.buffer, valid: false, datetime: Date.now() };\n\nsunspec.valid = sunspec.data[0] == 702 && sunspec.data[1] == 50 && sunspec.data.length == 52;\n\nlet msg1 = null\n\nif (sunspec.valid) {\n    sunspec.w_max = sunspec.data[2];\n    node.status({ fill: \"grey\", shape: \"dot\", text: 'Max ' + sunspec.data[2] + 'W'});\n    msg1 = { topic: 'Victron/Settings/Prod Max', payload: sunspec.w_max }\n} else {\n    node.status({ fill: 'red', shape: 'dot', text: 'Invalid data' });\n}\n\nflow.set('sunspec_702', sunspec);\n\nreturn msg1;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":370,"y":100,"wires":[[]]},{"id":"93891e2e7ae13fb4","type":"comment","z":"77403a7b4cd9572e","name":"Collecter les SunSpec depuis la passerelle Enphase","info":"","x":290,"y":60,"wires":[]},{"id":"441880e62df86138","type":"modbus-write","z":"77403a7b4cd9572e","name":"Limit Prod","showStatusActivities":true,"showErrors":true,"showWarnings":true,"unitid":"126","dataType":"MHoldingRegisters","adr":"40310","quantity":"5","server":"3df6c636aa6cabec","emptyMsgOnFail":false,"keepMsgProperties":false,"delayOnStart":false,"startDelayTime":"","x":300,"y":260,"wires":[[],[]]},{"id":"fa08059df2e441c5","type":"comment","z":"77403a7b4cd9572e","name":"Modifier la puissance de passerelle Enphase","info":"","x":270,"y":180,"wires":[]},{"id":"bfc20bd206637f9e","type":"inject","z":"77403a7b4cd9572e","name":"0%","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"0","payloadType":"num","x":110,"y":220,"wires":[["441880e62df86138"]]},{"id":"c5e751d86817c322","type":"inject","z":"77403a7b4cd9572e","name":"25%","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"25","payloadType":"num","x":110,"y":260,"wires":[["441880e62df86138"]]},{"id":"83ed3b888ad03a6e","type":"inject","z":"77403a7b4cd9572e","name":"100%","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"100","payloadType":"num","x":110,"y":300,"wires":[["441880e62df86138"]]},{"id":"3df6c636aa6cabec","type":"modbus-client","name":"Enphase Gateway","clienttype":"tcp","bufferCommands":true,"stateLogEnabled":false,"queueLogEnabled":false,"failureLogEnabled":true,"tcpHost":"192.168.219.12","tcpPort":"502","tcpType":"DEFAULT","serialPort":"/dev/ttyUSB","serialType":"RTU-BUFFERD","serialBaudrate":"9600","serialDatabits":"8","serialStopbits":"1","serialParity":"none","serialConnectionDelay":"100","serialAsciiResponseStartDelimiter":"0x3A","unit_id":"126","commandDelay":"5","clientTimeout":"60000","reconnectOnTimeout":true,"reconnectTimeout":"120000","parallelUnitIdsAllowed":true,"showErrors":true,"showWarnings":true,"showLogs":true}]

En conclusion, cette technologie est intéressante et permet bien d’intégrer la passerelle Enphase au système Victron mais elle a encore des défauts qui pourraient poser des problèmes aux personnes souhaitant réguler leur production avec le GX. Ceci est certainement une raison pour laquelle la gestion de la limitation de puissance peut être désactivée sur le GX.

Merci Quentin et kudos à toi,

Effectivement, la envoy semble vite congestionner en écoute du TCP 502 (modbus)

Du Cerbo en SSH, il m’est facile de provoquer une saturation avec 3 nc successif avec ce type de commande (ca se comporte un peu comme une API gateway qui fait du rate limit, c’est flippant si c’est son CPU qui sature déjà…)

nc -vz @IP-de-envoy 502

J’aime beaucoup ton idée via NodeRED, mais si j’ai bien suivi, avec NodeRED tu as toujours le problème d’instabilité de la socket TCP 502 de l’envoy ou j’ai loupé un truc.

En tout cas de vais creuser, car je suis systématiquement avec le cas ou la envoy met le cerbo à 0W et le matin ca ne repart pas sur les IQ7A

merci

Pour ma part, depuis que j’ai arrêté de faire des tests et modifications à tout va, c’est assez stable. Un seul dysfonctionnement en un mois.

En gros, tant qu’il n’y a qu’un seul client Modbus, ça va bien sauf lors d’un redémarrage de la passerelle. Plusieurs clients Modbus semblent poser un problème. Toutefois, j’ai déjà eu des périodes de plusieurs heures avec 2 clients sans problème.

J’avais publié un code générique de découverte et accès aux SunSpec sur Node-RED (Accessing SunSpec with Modbus TCP (flow) - Node-RED) dépendant de node-red-contrib-modbus.

top c’est ce qui me manquait

j’essaie de comprendre sunspec et ton code aide beaucoup merci

Bonjour Quentin

bon je suis intéressé pour voir comment tu régules au SOC 97%

déjà grace à ton code j’arrive en manuel a réguler avec des valeur static

et en effet, c’est stable avec les tempo que tu emploies dans NodeRed.

on semble être dans un cas d’interopérabilité avec d’un coté un modbus server asthmatique et un modbus client du cerbo qui envoie un peu trop car il récupère la télémétrie enphase en plus

Voici l’extrait de mon code qui régule la production lorsque c’est nécessaire.

Ce code n’est pas directement fonctionnel. Il doit d’être intégré à un code existant ou il faut le développer.

// w_max = dim702.data[2];
// Valeur maximale de production PV
// Cette valeur est nulle en début de journée et la nuit.
// Donc, j'utilise une valeur par défaut si c'est le cas (la valeur de veille).
// Cette valeur peut changer si un ou plusieurs micro-onduleurs ont un défaut.

if (envoy_ss.dim702_valid && envoy_ss.dim704_valid && envoy_ss.w_max > 0) {
    // Contrôle de la production par la limitation de puissance via SunSpec 704 et 
    //  par la limitation d'exportation si le contrôle par limitation de puissance ne régule pas bien

    limit_prod_enable = 1;
    
    // progress: Il s'agit d'un indice de progression (0-100) de la journée PV.
    //   A 0%, le soleil n'est pas levé et aucune production n'est possible
    //   A 100%, le soleil est couché et aucune production n'est possible
    // Il s'agit d'une courbe gaussienne attribuant plus d'importance aux heures les plus productives
    // Les coefficients et les heures définissant cette courbe sont founis par ENEDIS
    
    if (forecasts_valid && forecasts.j0.export == 0 && progress >= 99) {
        // Il n'y a plus de probabilité d'exporter
        // Donc, pas de nécessité de brider l'installation
        text += '.0';
        
        limit_prod_enable = 0;
        limit_prod_pct = 100;
    } else if (soc >= 99) {
        // Sécurité
        text += '.1';
        
        // Augmenter la limite d'exportation progressivement (480W, 120W, 0W)
        bridage_value = cur_bridage_value === null ? 3 : cur_bridage_value >= 3 ? 3 : cur_bridage_value + 1;
        
        // Réduire la production PV à 0
        limit_prod_pct = 0;
    } else {
        text += '.2';
        
        // Valeur de tolérance admise (4%)
        let delta_prod_w = Math.round(envoy_ss.w_max * 4 / 100);
        
        // Limitation de charge dynamique (hors CCL)
        // 4000W à l'entrée du convertisseur font 70A à la sortie DC

        let batt_limit = 0;
        if (soc < 90 || (forecasts_valid && forecasts.j0.export == 0 && progress >= 85)) batt_limit = 4000;
        else if (soc == 98) batt_limit = 0;
        else if (soc == 97) batt_limit = 250;
        else if (soc == 96) batt_limit = 500;
        else if (soc == 95) batt_limit = 1000;
        else if (soc == 94) batt_limit = 1500;
        else if (soc == 93) batt_limit = 2000;
        else if (soc == 92) batt_limit = 2500;
        else if (soc == 91) batt_limit = 3000;
        else if (soc == 90) batt_limit = 3500;
        else batt_limit = 4000;
        
        // Limitation de charge dynamique (CCL)
        // batt_w_limit = battery_ccl * battery_volt

        if (batt_limit > batt_w_limit*1.1) batt_limit = batt_w_limit*1.1;       // +10% lié à la perte de converssion AC-DC
        
        // cons_w = consommation totale (normal et secouru)
        // Quel est le pct nécessaire pour couvrir la consommation et recharger la batterie à l'intensité maximale admissible ?
        let prod_req_pct = Math.round((cons_w + batt_limit) / envoy_ss.w_max * 100);
        
        // Ajustement dynamique (pas ou peu nécessaire)
        // Utilité à vérifier...
        if (prod_req_pct > envoy_ss.limit_pct) {
            limit_prod_pct = (-grid_w > 2*delta_prod_w) ? prod_req_pct - 4 : prod_req_pct;
        } else if (prod_req_pct == envoy_ss.limit_pct) {
            limit_prod_pct =  (-grid_w > delta_prod_w) ? prod_req_pct - 4 : prod_req_pct;
        } else {
            limit_prod_pct = envoy_ss.limit_pct - prod_req_pct > 8 ? envoy_ss.limit_pct - Math.round((envoy_ss.limit_pcts - prod_req_pct) / 2) : prod_req_pct;
        }
        
        // Garder la limite entre 0 et 94 (~ 5400W)
        // 94 est éqal au PF imposé par la VRF
        // Donc, il ne faut pas espérer produire plus que 94%
        if (limit_prod_pct > 94) limit_prod_pct = 94;
        if (limit_prod_pct < 0) limit_prod_pct = 0;
        
        // Limite de production en W
        let limit_prod_w = Math.round(envoy_ss.w_max * envoy_ss.limit_pct / 100);
        
        // Y-a-t-il une surproduction ?
        let over_prod = context.get('over_prod');
        if (typeof over_prod !== 'number') over_prod = 0;
        
        if (prod_w > limit_prod_w + delta_prod_w) {
            if (over_prod < 3) over_prod++;
        } else if (prod_w < limit_prod_w) {
            if (over_prod > 0) over_prod--;
        }
        
        context.set('over_prod', over_prod);
        
        // S'il y a une surproduction,
        // Activer la limitation d'exportation progressivement (480W, 120W, 0W)
        if (over_prod == 3) {
            bridage_value = cur_bridage_value === null ? 3 : cur_bridage_value >= 3 ? 3 : cur_bridage_value + 1;       // Abaisser la limite d'exportation (100% --> 0%)
        } else if (over_prod > 0 && over_prod < 3) {
            bridage_value = cur_bridage_value === null ? 0 : cur_bridage_value > 0 ? cur_bridage_value - 1 : 0;        // Augmenter la limite d'exportation (0 % --> 100%)
        } else {
            bridage_value = 0;
        }
    }
    
    // Appliquer les nouveaux paramètres uniquement s'ils sont différents des précédents
    // Une demande de modification bloque la passerelle pendant 30s.
    // Donc, c'est mieux d'éviter les demandes inutiles.
    limit_prod_apply = envoy_ss.limit_enable != limit_prod_enable || envoy_ss.limit_pct != limit_prod_pct
} else {
    // Contrôle par la limitation d'exportation via les entrées numériques
    //   Uniquement lorsque les données SunSpec sont invalides
    
    // Prod <= 4000W -> 100% absorbé par batterie (70A) et conso résiduelle => Limit export à 480W (plus dynamique)
    // Prod >  4000W -> Risque d'exportation => Limit export à 120W (moins dynamique)
    
    let battery_charging = flow.get('battery_charging');
    if (typeof battery_charging !== 'boolean') {
        battery_charging = false;
    }
    
    if (soc >= 95) {
        // Sécurité
        bridage_value = 3;      // Limit export à 0W
        flow.set('battery_charging', false);
    } else if (soc >= 94 && battery_charging) {
        // Mettre en décharge
        bridage_value = 3;      // Limit export à 0W
        flow.set('battery_charging', false);
    } else if (soc <= 90 && !battery_charging) {
        // Mettre en charge
        bridage_value = soc < 85 && (batt_w < 2500 || prod_w < 4000) && grid_w > -500 ? 1 : 2;      // Limit export à 480W ou 120W
        flow.set('battery_charging', true);
    } else {
        if (battery_charging || grid_w > 480) {
            // Maintenir ou mettre en charge
            bridage_value = soc < 85 && (batt_w < 2500 || prod_w < 4000) && grid_w > -500 ? 1 : 2;  // Limit export à 480W ou 120W
        } else {
            // Maintenir ou remettre en décharge
            bridage_value = 3;      // Limit export à 0W
        }
    }
}

Je n’aime pas trop la façon dont réagit la passerelle Enphase en cas de dépassement de la limite d’exportation. A chaque fois, la production PV est subitement fortement réduite entraînant une variation d’intensité difficile à gérer instantanément par le GX (en mode en ligne). Ceci induit un pic d’importation pendant quelques secondes. C’est pas énorme mais ça fait tourner le compteur d’importation inutilement.

La gestion par limitation de production n’a pas ce problème. Elle est beaucoup plus simple à gérer pour le GX.

super merci à toi,

du coup, a force de discuter, je me demande si je ne vais pas changer de stratégie

voici mon code pour la situation off-grid, ou je fait ma limitation de prod Enphase avec le relai cerbo vers entrée I/0 enphase

[
    {
        "id": "661534b335cf2e3b",
        "type": "tab",
        "label": "Flux 1",
        "disabled": false,
        "locked": true,
        "info": "",
        "env": []
    },
    {
        "id": "de64d2ba097fdaf6",
        "type": "function",
        "z": "661534b335cf2e3b",
        "name": "store to variable \"SOC\"",
        "func": "flow.set(\"SOC\",msg.payload);\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 390,
        "y": 240,
        "wires": [
            [
                "f00e8acbc0c22101"
            ]
        ]
    },
    {
        "id": "59901ea48d798ab3",
        "type": "victron-input-system",
        "z": "661534b335cf2e3b",
        "service": "com.victronenergy.system/0",
        "path": "/Dc/Battery/Soc",
        "serviceObj": {
            "service": "com.victronenergy.system/0",
            "name": "Venus system"
        },
        "pathObj": {
            "path": "/Dc/Battery/Soc",
            "type": "float",
            "name": "Battery State of Charge (%)"
        },
        "initial": "",
        "name": "SOC",
        "onlyChanges": false,
        "roundValues": "1",
        "x": 110,
        "y": 240,
        "wires": [
            [
                "de64d2ba097fdaf6"
            ]
        ]
    },
    {
        "id": "2f0aedb54c994da3",
        "type": "function",
        "z": "661534b335cf2e3b",
        "name": "Hysteresis Power management: Reduce on SOC > 90% , reactivate on SOC < 70%",
        "func": "// node.warn(msg.payload);\nlet soc = msg.payload.SOC;\nlet systemState = msg.payload.GRID_LOST;\n\n// Mode \"off-grid\" = 2\nlet isOffGrid = (systemState === 2);\n\n// Récupère l'état précédent (default: false)\nlet previousRelayState = flow.get(\"relayState\") || false;\nlet newRelayState = previousRelayState;\n\nif (isOffGrid) {\n    if (soc > 90) {\n        newRelayState = true;\n    } else if (soc < 70) {\n        newRelayState = false;\n    }\n    // Sinon, on garde l'état précédent (hystérésis)\n} else {\n    newRelayState = false;  // Sécurité : on coupe si pas en off-grid\n}\n\n// Sauvegarde l'état actuel pour la prochaine fois\nflow.set(\"relayState\", newRelayState);\n\n// 1 = on reduit la puissance a zero\nreturn { payload: newRelayState ? 1 : 0 };",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 510,
        "y": 740,
        "wires": [
            [
                "82da9483ad10c9f1",
                "2761648901bf06bf"
            ]
        ]
    },
    {
        "id": "82da9483ad10c9f1",
        "type": "debug",
        "z": "661534b335cf2e3b",
        "name": "debug 4",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 760,
        "y": 640,
        "wires": []
    },
    {
        "id": "fa2318ef35fcd888",
        "type": "function",
        "z": "661534b335cf2e3b",
        "name": "store to variable \"GRID_LOST\"",
        "func": "flow.set(\"GRID_LOST\",msg.payload);\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 410,
        "y": 440,
        "wires": [
            [
                "fb2f5059e94a4e42"
            ]
        ]
    },
    {
        "id": "f00e8acbc0c22101",
        "type": "change",
        "z": "661534b335cf2e3b",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "{ \"SOC\": $number(payload) }",
                "tot": "jsonata"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 690,
        "y": 240,
        "wires": [
            [
                "c3a5d12a9015774a"
            ]
        ]
    },
    {
        "id": "fb2f5059e94a4e42",
        "type": "change",
        "z": "661534b335cf2e3b",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "{ \"GRID_LOST\": $number(payload) }",
                "tot": "jsonata"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 690,
        "y": 440,
        "wires": [
            [
                "c3a5d12a9015774a"
            ]
        ]
    },
    {
        "id": "c3a5d12a9015774a",
        "type": "join",
        "z": "661534b335cf2e3b",
        "name": "",
        "mode": "custom",
        "build": "merged",
        "property": "payload",
        "propertyType": "msg",
        "key": "parts",
        "joiner": "\\n",
        "joinerType": "str",
        "accumulate": false,
        "timeout": "",
        "count": "2",
        "reduceRight": false,
        "reduceExp": "",
        "reduceInit": "",
        "reduceInitType": "",
        "reduceFixup": "",
        "x": 920,
        "y": 340,
        "wires": [
            [
                "4f2260adc03d1f70"
            ]
        ]
    },
    {
        "id": "0ca5b8fabd9a7570",
        "type": "inject",
        "z": "661534b335cf2e3b",
        "name": "SOC 69%",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "69",
        "payloadType": "num",
        "x": 120,
        "y": 180,
        "wires": [
            [
                "de64d2ba097fdaf6"
            ]
        ]
    },
    {
        "id": "b415a0b2b3ab8dac",
        "type": "inject",
        "z": "661534b335cf2e3b",
        "name": "SOC 71%",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "71",
        "payloadType": "num",
        "x": 120,
        "y": 140,
        "wires": [
            [
                "de64d2ba097fdaf6"
            ]
        ]
    },
    {
        "id": "dfb5dc6aca9ac5a5",
        "type": "inject",
        "z": "661534b335cf2e3b",
        "name": "SOC 89%",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "89",
        "payloadType": "num",
        "x": 120,
        "y": 100,
        "wires": [
            [
                "de64d2ba097fdaf6"
            ]
        ]
    },
    {
        "id": "63438a8270c77154",
        "type": "inject",
        "z": "661534b335cf2e3b",
        "name": "SOC 91%",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "91",
        "payloadType": "num",
        "x": 120,
        "y": 60,
        "wires": [
            [
                "de64d2ba097fdaf6"
            ]
        ]
    },
    {
        "id": "4f2260adc03d1f70",
        "type": "delay",
        "z": "661534b335cf2e3b",
        "name": "12 messages / hour (relay protect)",
        "pauseType": "rate",
        "timeout": "5",
        "timeoutUnits": "seconds",
        "rate": "12",
        "nbRateUnits": "1",
        "rateUnits": "hour",
        "randomFirst": "1",
        "randomLast": "5",
        "randomUnits": "seconds",
        "drop": true,
        "allowrate": false,
        "outputs": 1,
        "x": 1200,
        "y": 340,
        "wires": [
            [
                "2f0aedb54c994da3"
            ]
        ]
    },
    {
        "id": "3edc756d18e0e240",
        "type": "inject",
        "z": "661534b335cf2e3b",
        "name": "GRID OFF",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "2",
        "payloadType": "num",
        "x": 120,
        "y": 540,
        "wires": [
            [
                "fa2318ef35fcd888"
            ]
        ]
    },
    {
        "id": "7a6f73f5f9b05fbd",
        "type": "inject",
        "z": "661534b335cf2e3b",
        "name": "Grid ON",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "0",
        "payloadType": "num",
        "x": 120,
        "y": 500,
        "wires": [
            [
                "fa2318ef35fcd888"
            ]
        ]
    },
    {
        "id": "2761648901bf06bf",
        "type": "victron-output-relay",
        "z": "661534b335cf2e3b",
        "service": "com.victronenergy.system/0",
        "path": "/Relay/0/State",
        "serviceObj": {
            "service": "com.victronenergy.system/0",
            "name": "Venus device"
        },
        "pathObj": {
            "path": "/Relay/0/State",
            "type": "enum",
            "name": "Venus relay 1 state",
            "enum": {
                "0": "Open",
                "1": "Closed"
            },
            "writable": true,
            "disabled": false
        },
        "initial": "0",
        "name": "triggering Cerbo GX relay 1",
        "onlyChanges": false,
        "x": 1060,
        "y": 740,
        "wires": []
    },
    {
        "id": "43990609ab93ccad",
        "type": "victron-input-vebus",
        "z": "661534b335cf2e3b",
        "service": "com.victronenergy.vebus/276",
        "path": "/Alarms/GridLost",
        "serviceObj": {
            "service": "com.victronenergy.vebus/276",
            "name": "BIG MAMA"
        },
        "pathObj": {
            "path": "/Alarms/GridLost",
            "type": "enum",
            "name": "Grid lost alarm",
            "enum": {
                "0": "Ok",
                "2": "Alarm"
            }
        },
        "initial": "",
        "name": "Grid lost",
        "onlyChanges": false,
        "x": 120,
        "y": 440,
        "wires": [
            [
                "fa2318ef35fcd888"
            ]
        ]
    }
]

et la je me dit que j’ai peut-être une solution qui règle quasi tout mes régime OFF/ON/Grid

Off-grid:

j’adapte mon code NodeRed pour que mon hysteresis limite la puissance de Prod enphase via le Modbus enphase sunspec

du coup ca libère la fonction de limitation d’export via Production d’électricité limitée par envoy (enlighten)

et donc pour On-Grid

et bien je m’appuie sur le grid progfile enphase PEL 2kW en attendant que le modbus enphase soit corrigé

et le meilleur pour la fin : je passe en limitation export 300W via Production d’électricité limitée par envoy (enlighten)

je crois bien que je vais partir la dessus

En mode hors-réseau, c’est une bonne chose de limiter la production des micro-onduleurs Enphase. Ça évitera de faire monter la fréquence pour que la limitation se fasse.

En mode réseau, s’il t’est nécessaire d’avoir une limitation d’exportation permanente (et non dynamique) alors, je pense que ce sera mieux de la mettre dans enlighten (soit via le profil réseau, soit par la limitation d’exportation). Si tu as une limitation d’exportation dynamique (autorisé si le prix de vente est positif, interdit si le prix de vente est négatif), alors le contrôle de la production couplé à une limitation basée sur les entrées numériques pilotées par les relais du Cerbo est une bonne solution.

Bon courage pour l’écriture du code. Ça peut être long.

exactement

ca va le faire, en tout cas avec la limitation statique d’export a 300/500 W j’ai déja des oscillation un peu embêtante lors de la phase d’absorption, en gros le multiplus n’arrive pas a stabiliser le courant et le voltage d’absorption

pas gênant car c’est du LTO ici

en tout cas le code est deja fonctionnel, faut juste que je change l’action de relay vers modbus

c’est pas mal la gestion off-grid que j’ai actuellement, car j’ai un hysteresis: en gros si SOC 100% => PV prod = 0%, SOC descend a genre 70% et la pv prod 100%. comme ca pas d’oscillation en off grid, ca marche nickel. adios Frequency daubing :slight_smile:

juste pour info, il ne stabilise pas le courant et la tension, soit il est en bulk et c’est un generateur de courant, soit il est en absorption et c’est un generateur de tension il ne peut pas faire les deux en même temps!

oui,

en fait comme les micro-inverter enphase réagissent trop brusquement ca fait tomber la tension un peu trop vite et du coup ben y a moins de courant qui peut rentrer dans les batteries. Et donc ca oscille pas mal

dans mon cas ca doit envoyer 5A / batterie et ca oscille entre 2A et 7A /batterie avec une moyenne a 3,5 A / bat. bref pas super gênant, en fait c’est une courbe d’absorption similaire a des passages de nuage actif

merci

D’ailleurs en y repensant, il y a moyen de faire une gestion de l’absorption aux petits oignons en NodeRed avec le dev de Quentin une fois de plus

Donc, cette fois ci, en situation On-Grid, sur event start ABS je vais limiter la puissance des enphase PV à une valeur qui donne ce qui est globalement nécessaire à 5A par batterie en plus du fond de conso des load

et je stoppe on event ABS => FLOAT, et la c’est tjs le capp export qui prend le relai (300 Watt export max)

je faisais un truc similaire dans le passé en Homeassistant en m’aidant de l’apport Grid pour soigner les phases d’absorption lors du rodage des batteries en m’aidant du ACpowerSetpoint (car avec des nuages en mode ESS c’était compliqué)

On est à deux doigt d’avoir stabilisé et résolu completement la plupart des default de l’AC coupling !

Bonjour,

Mon expérience n’a pas été particulièrement bonne, principalement parce que le temps de réponse de la limitation de puissance d’Enphase a été assez lent, du moins dans mon cas.

Au début, j’ai utilisé un proxy pour permettre à Node-RED et au Cerbo GX d’accéder simultanément à l’Envoy via Modbus TCP, puisque l’Envoy (du moins actuellement) ne permet qu’une seule connexion Modbus TCP active. C’est une petite limitation qu’il faut prendre en compte.

Une fois l’accès simultané résolu grâce au proxy, il est devenu possible de gérer la limitation de puissance depuis Node-RED et le Cerbo GX. Cependant, comme mentionné précédemment, la vitesse de réponse n’était toujours pas suffisante dans mon cas et la régulation intervenait trop tard.

Ma solution actuelle est différente : j’utilise le Cerbo GX uniquement pour accéder à l’Envoy et récupérer les données de production. Pour le contrôle de l’exportation, j’utilise un profil réseau « Sonnen - No Export », qui permet un export maximum vers le réseau d’environ 200 W.

De plus, j’utilise deux TC (transformateurs de courant) pour la surveillance du réseau :

  • Un TC installé à l’entrée principale du réseau.

  • Un autre TC installé sur un circuit de charge de la maison qui maintient toujours une petite consommation électrique positive.

Ces TC sont commutés automatiquement en fonction du prix de l’énergie à l’exportation :

  • Lorsque le prix de l’exportation est négatif, j’utilise le TC installé à l’entrée du réseau.

  • Lorsque le prix de l’exportation est positif, j’utilise le TC connecté au circuit de charge toujours actif.

Naturellement, cette approche présente un inconvénient : les statistiques d’Enphase concernant l’énergie exportée ne son plus exactes et ne peuvent donc plus être considérées comme pleinement valides.

Pour automatiser l’ensemble du processus, j’utilise Home Assistant pour récupérer les informations sur les prix de l’exportation et pour contrôler un relais chargé de commuter les TC. J’utilise également un appareil Shelly pour surveiller la ligne du réseau, et j’envoie toutes les données collectées à Grafana pour la visualisation.

Jusqu’à présent, c’est la seule solution que j’ai trouvée qui offre une régulation de puissance suffisamment rapide et efficace en fonction du prix de l’énergie à l’exportation.

Merci de ton retour qui confirme que le temps de réponse au changement de limitation de puissance en production est toujours un problème actuellement. Il était d’environ 30 secondes quand j’avais fait mes tests, il y a presque un an. Je présume que rien n’a changé de ce point de vue. Idem, pour le problème de session Modbus TCP multiple. La meilleure approche reste une session unique, soit via le GX soit via Node-RED. Il est possible d’utiliser une session Node-RED pour ensuite injecter les données sous forme d’appareil virtuel pour en informer le GX.

Ton idée de proxy est bonne mais certainement pas évidente pour la majorité des utilisateurs.

En revanche, j’ai du mal à comprendre le fonctionnement de ton système de commutation des transformateurs de courant.

Je ne sais pas à quel rythme les prix peuvent fluctuer pour toi. Tu sembles être préoccupé par un changement instantané mais, d’un autre côté, tu admets un export maximum d’environ 200W, je suppose en prix négatif.

As-tu un dispositif de stockage ? Si oui, en cas de prix négatif, il te suffit de mettre ton installation de production à l’arrêt dès que tes batteries sont presque au maximum (par exemple entre 90 et 100%) et de reprendre la production lorsqu’elles passent en dessous d’un certain seuil (par exemple entre 80 et 95%). C’est assez simple à programmer sous Node-RED.

Bonjour à nouveau,

Lors de mes essais, j’ai estimé un temps compris entre 20 et 25 secondes, bien qu’il puisse être d’environ 30 secondes comme vous l’indiquez.

J’ai également testé Node-RED afin de pouvoir utiliser plusieurs sessions Modbus TCP, mais j’ai finalement choisi d’utiliser un proxy qui me permettait d’obtenir les journaux de toutes les expérimentations et de déboguer l’ensemble du trafic. C’est une solution plus complexe, comme vous le mentionnez.

Actuellement, les tarifs sont souvent négatifs pendant les heures centrales de la journée (5 à 6 heures consécutives). Dans ce cas, l’Envoy récupère la mesure du CT réseau et les micro-onduleurs appliquent un profil réseau qui n’autorise qu’une injection d’environ 200 W sur le réseau. J’ai également testé un profil avec injection zéro, mais la régulation n’était pas aussi précise qu’avec le profil limité à 200 W. Lorsque le prix de l’exportation devient positif, le CT est commuté vers une ligne de la maison où il y a toujours une certaine consommation (lecture positive), et dans ce cas la production photovoltaïque passe à sa puissance maximale possible.

Je dispose également d’un système de stockage géré via Node-RED, où j’ajuste l’objectif réseau, le SOC et le courant de charge. Cependant, après tous les essais réalisés, pour la régulation de la production, cette solution s’est révélée être la meilleure dans mon cas.