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.

4 Likes

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.

1 Like

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.

1 Like

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 !