Relais et 230VCA

Dans la documentation du Cerbo, il est inscrit

Respectez les limites de tension et de courant des relais, qui sont les suivantes :

  • CC jusqu’à 30 VCC : 6 A
  • CC jusqu’à 70 VCC : 1 A
  • CA : 6 A, 125 VCA

D’un côté, j’ai des contacteurs et rupteurs modulaires qui se commandent en 230VCA. De l’autre côté, j’ai les relais du Cerbo (ou Multiplus) qui n’acceptent pas le 230VCA d’après leur documentation. Le circuit de commande est protégé par un disjoncteur 2A.

Quel risque y-a-t-il à raccorder le 230VCA sur ce relais ?

Que suggérez-vous comme solution ?

Pour ma part je ne passerais pas la partie commande des relais dans les relais su cerbo.
En général on utilise des relais 12v ou 24v piloté par les relais du cerbo.
Sinon tu peux utiliser des interrupteurs sans fil c’est simple à câbler mais un peu honéreux.

Tout a fait d’accord avec @Josselin_Carapace à plus forte raison pour piloter du CA

1 Like

Merci pour vos réponses.

Après la lecture de la publication ci dessous, j’hésite encore plus à utiliser les relais du Cerbo pour piloter un contacteur ou rupteur même en passant par un circuit 12 ou 24 V (DC ou AC).

Pour un des relais, j’ai déjà un projet d’essayer de l’utiliser pour piloter la passerelle Enphase via les Digital Inputs. J’attends des journées ensoleillées pour m’y mettre. Il n’en resterait alors qu’un de libre, suffisant pour piloter un chauffe-eau. Toutefois, j’ai aussi l’idée de piloter d’autres circuits, notamment afin de les couper avec des rupteurs lors de journées Tempo Rouge.

En tous cas, passer par un étage à 12 ou 24V nécessite d’avoir une alimentation et un contacteur adaptés que je n’ai actuellement pas, donc des investissements à faire. En alternative, j’envisage de placer un Waveshare PoE Ethernet Relay qui serait en charge de piloter les contacteurs ou rupteurs que je possède déjà, tout comme le PoE.

A priori, il serait même pilotable à partir de Node-RED. Ma première inquiétude est la fiabilité à moyen et long terme de ce produit. Ma seconde est l’acceptation par le Consuel.

1 Like

Après moult hésitations, j’ai finalement opté pour un Shelly Pro 2 qui a l’avantage d’avoir un tas de certifications que le Waveshare n’a pas. Il est pilotable par HTTP et MQTT. Dès lors, je pense qu’il n’y aura aucun problème pour le piloter à partir de Node-RED.

Personnellement, n’ayant pas suffisamment confiance en la qualité des relais intégrés à cet appareil, je ne l’utiliserai pas directement pour contrôler le chauffe-eau mais bien pour contrôler le contacteur du chauffe-eau, de même pour les autres circuits.

En attente de livraison.

1 Like

Après mise en oeuvre, le Shelly Pro 2 fait bien le job souhaité.

Je pensais qu’il intégrait son propre serveur MQTT mais ce n’est pas le cas. Il prend ces commandes et publie son status sur un serveur externe local (Cerbo ou autre) ou distant (Web). Sur ce point, j’ai perdu un peu de temps.

Étant en couplage AC, j’ai écrit un algorithme de gestion du chauffe-eau afin de privilégier la chauffe dès que la production photovoltaïque le permet en minimisant l’énergie consommée à partir des batteries tout en garantissant que, si la production ne permet pas de terminer un cycle de chauffe complet, la chauffe complète ait au moins lieu tous les 3 jours en heures creuses sans consommer d’énergie provenant de la batterie.

Après 1 semaine de mise en oeuvre, je suis vraiment satisfait du résultat.

2 Likes

Cool il va falloir que tu nous publies tout cela :wink:

1 Like

J’ai essayé d’isoler le code de gestion du chauffe-eau par la commande du relais du Shelly.

Ce n’est pas un code simple mais ça pourra peut-être donner des inspirations à l’un ou l’autre.

[{"id":"55d2634b4497f676","type":"tab","label":"Flow 1","disabled":false,"info":"","env":[]},{"id":"5ff1344d73c6e38b","type":"inject","z":"55d2634b4497f676","name":"10:00 - 19:00 / 1m","props":[{"p":"timestamp","v":"","vt":"date"},{"p":"reset","v":"1","vt":"num"}],"repeat":"","crontab":"*/1 10-18 * * *","once":false,"onceDelay":0.1,"topic":"","x":140,"y":600,"wires":[["dabd14eee03bc774"]]},{"id":"dabd14eee03bc774","type":"trigger","z":"55d2634b4497f676","name":"Trigger & Block","op1":"","op2":"","op1type":"pay","op2type":"nul","duration":"0","extend":false,"overrideDelay":false,"units":"s","reset":"","bytopic":"all","topic":"topic","outputs":1,"x":390,"y":600,"wires":[["ca8e5dbc4888246a"]]},{"id":"ca8e5dbc4888246a","type":"rbe","z":"55d2634b4497f676","name":"Ignore 1st msg","func":"rbei","gap":"","start":"","inout":"out","septopics":false,"property":"payload","topi":"topic","x":590,"y":600,"wires":[["876623b03edd0ffa"]]},{"id":"71d2d972e5977e7b","type":"comment","z":"55d2634b4497f676","name":"Gestion Chauffe-Eau","info":"","x":130,"y":560,"wires":[]},{"id":"5f3cc63bcd6f6afa","type":"inject","z":"55d2634b4497f676","name":"01:00 - 06:00 / 5m","props":[{"p":"timestamp","v":"","vt":"date"},{"p":"reset","v":"1","vt":"str"}],"repeat":"","crontab":"*/5 1-5 * * *","once":false,"onceDelay":0.1,"topic":"","x":140,"y":720,"wires":[["f198134add478f22"]]},{"id":"f198134add478f22","type":"trigger","z":"55d2634b4497f676","name":"Trigger & Block","op1":"","op2":"","op1type":"pay","op2type":"nul","duration":"0","extend":false,"overrideDelay":false,"units":"s","reset":"","bytopic":"all","topic":"topic","outputs":1,"x":390,"y":720,"wires":[["9a41d899fa18f400"]]},{"id":"9a41d899fa18f400","type":"rbe","z":"55d2634b4497f676","name":"Ignore 1st msg","func":"rbei","gap":"","start":"","inout":"out","septopics":false,"property":"payload","topi":"topic","x":590,"y":720,"wires":[["a8263dff501415be"]]},{"id":"876623b03edd0ffa","type":"function","z":"55d2634b4497f676","name":"Chauffe-Eau HP","func":"// Fonction qui ajoute un 0 devant un nombre pour compléter la chaine de caractères\nconst pad = n => {\n    return `${Math.floor(Math.abs(n))}`.padStart(2, '0');\n}\n// Fonction qui retourne la date au format 'dd/mm hh:ii'\nconst toDateString = date => {\n    return pad(date.getDate()) + '/' + pad(date.getMonth() + 1) + ' ' + pad(date.getHours()) + ':' + pad(date.getMinutes())\n}\n// Fonction qui retourne la date au format 'hh:ii'\nconst toTimeString = date => {\n    return pad(date.getHours()) + ':' + pad(date.getMinutes())\n}\n\nlet date = new Date();\nlet timestamp = date.getTime();\n\nlet text = '';\nlet fill = 'grey';\n\nlet error = false;\nlet shelly_1_closed = flow.get('shelly_1_closed');\nif (typeof shelly_1_closed !== 'boolean') {\n    error = true;\n    text = 'Invalid shelly_1_closed: ' + shelly_1_closed;\n    shelly_1_closed = true;\n    flow.set('shelly_1_closed', shelly_1_closed);\n}\n\nlet ce_on_delay = flow.get('ce_on_delay');\nif (ce_on_delay !== 0 && ce_on_delay !== 1 && ce_on_delay !== 2) {\n    error = true;\n    text = 'Invalid ce_on_delay: ' + ce_on_delay;\n    flow.set('ce_on_delay', 0);\n}\n\nlet ce_off_delay = flow.get('ce_off_delay');\nif (ce_off_delay !== 0 && ce_off_delay !== 1 && ce_off_delay !== 2) {\n    error = true;\n    text = 'Invalid ce_off_delay: ' + ce_off_delay;\n    flow.set('ce_off_delay', 0);\n}\n\nlet ce_full_delay = flow.get('ce_full_delay');\nif (ce_full_delay !== 0 && ce_full_delay !== 1 && ce_full_delay !== 2) {\n    error = true;\n    text = 'Invalid ce_full_delay: ' + ce_full_delay;\n    ce_full_delay = 0;\n    flow.set('ce_full_delay', ce_full_delay);\n}\n\nlet ce_full_timestamp = flow.get('ce_full_timestamp');\nif (typeof(ce_full_timestamp) !== 'number' || ce_full_timestamp > timestamp) {\n    error = true;\n    text = 'Invalid ce_full_timestamp: ' + ce_full_timestamp;\n    ce_full_timestamp = timestamp - 52*60*60*1000; // 52h avant maintenant\n    flow.set('ce_full_timestamp', ce_full_timestamp);\n}\n\nlet ess_state = msg.payload['ESS State'];\nif (typeof ess_state !== 'number' || ess_state < 1 || ess_state > 12) {\n    error = true;\n    text = 'Invalid ess_state: ' + ess_state;\n    ess_state = 0\n}\n\nlet soc = msg.payload['SoC'];\nif (typeof soc !== 'number' || soc < 5 || soc > 100) {\n    error = true;\n    text = 'Invalid soc: ' + soc;\n    soc = 5;\n}\n\n// Récupération des valeurs de l'algorithme BatteryLifeQ\nlet blq_default_soc_min = global.get('blq_default_soc_min');\nif (typeof blq_default_soc_min !== 'number' || blq_default_soc_min < 10) {\n    blq_default_soc_min = 10;\n    global.set('blq_default_soc_min', blq_default_soc_min);\n} else if (blq_default_soc_min > 85 ) {\n    blq_default_soc_min = 85;\n    global.set('blq_default_soc_min', blq_default_soc_min);\n}\n\nlet blq_active_soc_limit = global.get('blq_active_soc_limit');\nif (typeof blq_active_soc_limit !== 'number' || blq_active_soc_limit < blq_default_soc_min) {\n    blq_active_soc_limit = blq_default_soc_min;\n    global.set('blq_active_soc_limit', blq_active_soc_limit);\n} else if (blq_active_soc_limit > 85 ) {\n    blq_active_soc_limit = 85;\n    global.set('blq_active_soc_limit', blq_active_soc_limit);\n}\n\n// Récupération de la valeur de production PV\nlet prod_w = msg.payload['PV Power'];\nif (typeof prod_w !== 'number' || prod_w < -15) {\n    error = true;\n    error_msg = 'Invalid prod_w: '+prod_w;\n    prod_w = 0;\n}\n\n// Récupération de la valeur de batterie\nlet bat_w = msg.payload['Battery Power'];\nif (typeof bat_w !== 'number' || bat_w < -9000 || bat_w > 4000) {\n    error = true;\n    error_msg = 'Invalid bat_w: '+bat_w;\n    bat_w = -9000;\n}\n\n// Récupération de la valeur de consommation réseau\nlet grid_w = msg.payload['Grid Power'];\nif (typeof grid_w !== 'number' || grid_w < -6000 || grid_w > 6000) {\n    error = true;\n    error_msg = 'Invalid grid_w: '+grid_w;\n    grid_w = 6000;\n}\n\n// Récupération de la valeurs de consommation des charges non-secourues\nlet normal_load_w = msg.payload['Normal Power'];\nif (typeof normal_load_w !== 'number' || normal_load_w < -1000 || normal_load_w > 10000) {\n    error = true;\n    text = 'Invalid normal_load_w: '+normal_load_w;\n    normal_load_w = 0;\n}\n\nce_command = 'off';\n\nif (error) {\n    // Eteindre le chauffe-eau\n    fill = 'red';\n} else if (timestamp - ce_full_timestamp < 14*60*60*1000) {\n    // La dernière chauffe date de moins de 14h -> Ne rien faire.\n    \n    // Réinitialiser le compteur d'attente OFF et FULL\n    if (ce_on_delay !== 0) { ce_on_delay = 0; flow.set('ce_on_delay', ce_on_delay); }\n    if (ce_off_delay !== 0) { ce_off_delay = 0; flow.set('ce_off_delay', ce_off_delay); }\n    if (ce_full_delay !== 0) { ce_full_delay = 0; flow.set('ce_full_delay', ce_full_delay); }\n    \n    // Eteindre le chauffe-eau si SoC <= 90 ou peu d'énergie absorbée ou injectée\n    if (soc >= 90) {\n        if (shelly_1_closed) {\n            // Relais est fermé -> Chauffe-eau allumé\n            if (bat_w > -2000) {\n                // Tant que la batterie ne délivre pas plus de 2000W, garder allumer\n                ce_command = 'on'\n            }\n            \n            if (normal_load_w < 1600) {\n                // Actualiser la valeur de la dernière chauffe complète\n                ce_full_timestamp = timestamp;\n                flow.set('ce_full_timestamp', ce_full_timestamp);\n            }\n        } else {\n            // Relais est ouvert -> Chauffe-eau éteint\n            if (bat_w-grid_w > 800 && prod_w > 2000) {\n                // Si la batteie et le réseau consomme plus de 800W venant de la production PV qui doit être supérieure à 2000W,\n                // Alors allumer le chauffe-eau\n                ce_command = 'on';\n            }\n        }\n    }\n    \n    // Afficher la valeur de la dernière chauffe complète\n    date.setTime(ce_full_timestamp);\n    text = 'Full ' + toDateString(date);\n} else if (![2,3,4,10].includes(ess_state)) {\n    // ESS State invalide pour chauffer en journée\n    \n    // Réinitialiser le compteur d'attente OFF et FULL\n    if (ce_on_delay !== 0) { ce_on_delay = 0; flow.set('ce_on_delay', ce_on_delay); }\n    if (ce_off_delay !== 0) { ce_off_delay = 0; flow.set('ce_off_delay', ce_off_delay); }\n    if (ce_full_delay !== 0) { ce_full_delay = 0; flow.set('ce_full_delay', ce_full_delay); }\n    \n    fill = 'yellow';\n    text = 'ESS State ' + ess_state;\n} else {\n    if (shelly_1_closed) {\n        // Relais est fermé -> Chauffe-eau allumé\n        \n        // Réinitialiser le compteur d'attente ON\n        if (ce_on_delay !== 0) { ce_on_delay = 0; flow.set('ce_on_delay', ce_on_delay); }\n        \n        let power = 200 + (soc - blq_active_soc_limit) * 20;\n        \n        // Si la somme d'énergie venant de la batterie et du réseau dépasse power.\n        if (normal_load_w < 1600) {\n            // La chauffe est terminée\n\n            // Réinitialiser le compteur d'attente OFF\n            if (ce_off_delay !== 0) { ce_off_delay = 0; flow.set('ce_off_delay', ce_off_delay); }\n            \n            // Attendre 3 minutes\n            if (ce_full_delay === 2) {\n                // Eteindre le chauffe-eau\n                \n                // Actualiser la valeur de la dernière chauffe complète\n                ce_full_timestamp = timestamp;\n                flow.set('ce_full_timestamp', ce_full_timestamp);\n                \n                // Afficher la valeur de la dernière chauffe complète\n                text = 'Full ' + toDateString(date);\n            } else {\n                // Incrémenter le compteur d'attente et garder le chauffe-eau allumé\n                flow.set('ce_full_delay', ++ce_full_delay);\n                text = 'Wait full ('+ce_full_delay+')';\n                ce_command = 'on';\n            }\n        } else if (grid_w-bat_w >= power) {\n            // La production est insuffisante -> Eteindre le chauffe-eau\n    \n            // Réinitialiser le compteur d'attente FULL\n            if (ce_full_delay !== 0) { ce_full_delay = 0; flow.set('ce_full_delay', ce_full_delay); }\n            \n            // Attendre 3 minutes\n            if (ce_off_delay === 2) {\n                // Eteindre le chauffe-eau\n                text = 'Turn Off';\n            } else {\n                // Incrémenter le compteur d'attente et garder le chauffe-eau allumé\n                flow.set('ce_off_delay', ++ce_off_delay);\n                text = 'Wait off ('+ce_off_delay+')';\n                ce_command = 'on';\n            }\n        } else {\n            // Réinitialiser les compteurs d'attente OFF et FULL\n            if (ce_off_delay !== 0) { ce_off_delay = 0; flow.set('ce_off_delay', ce_off_delay); }\n            if (ce_full_delay !== 0) { ce_full_delay = 0; flow.set('ce_full_delay', ce_full_delay); }\n            \n            // Attendre et garder le chauffe-eau allumé\n            text = 'Heating';\n            ce_command = 'on';\n        }\n    } else {\n        // Relais est ouvert -> Chauffe-eau éteint\n        \n        // Réinitialiser le compteur d'attente OFF et FULL\n        if (ce_off_delay !== 0) { ce_off_delay = 0; flow.set('ce_off_delay', ce_off_delay); }\n        if (ce_full_delay !== 0) { ce_full_delay = 0; flow.set('ce_full_delay', ce_full_delay); }\n        \n        // Limite de puissance admise\n        let power = 2400 - (soc - blq_active_soc_limit)*10;\n\n        if (bat_w-grid_w >= power) {\n            // Si la somme d'énergie chargée dans la batterie et envoyée sur le réseau dépasse la limite\n            // -> Rediriger l'énergie vers le chauffe-eau\n            \n            // Attendre 3 cycles (3 minutes)\n            if (ce_on_delay === 2) {\n                // Allumer le chauffe-eau\n                text = 'Turn on';\n                ce_command = 'on';\n            } else {\n                // Garder le chauffe-eau éteint\n                // Incrémenter le compteur d'attente\n                flow.set('ce_on_delay', ++ce_on_delay);\n                text = 'Wait on ('+ce_on_delay+')';\n            }\n        } else {\n            // Attendre et garder le chauffe-eau éteint\n            text = 'Wait '+power+'W';\n            \n            // Réinitialiser le compteur d'attente ON\n            if (ce_on_delay !== 0) { flow.set('ce_on_delay',0); ce_on_delay = 0; }\n        }\n    }\n}\n\nnode.status({fill:fill,shape:\"dot\",text: toTimeString(new Date()) + ' ' + text});\n\nlet msg1 = { topic: 'Chauffe-Eau', payload: (ce_command == 'on') ? ce_command : 'off' };\n\nreturn msg1;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":790,"y":600,"wires":[["842b2a831be1d11a"]]},{"id":"ad5fb77da3ee20e2","type":"inject","z":"55d2634b4497f676","d":true,"name":"ON","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":190,"y":760,"wires":[["109c93bf8ec11a63"]]},{"id":"0e2ab93de35a86c5","type":"inject","z":"55d2634b4497f676","name":"19:01","props":[{"p":"payload"}],"repeat":"","crontab":"01 19 * * *","once":false,"onceDelay":0.1,"topic":"","payload":"off","payloadType":"str","x":180,"y":640,"wires":[["c2c5ef671b126fa8"]]},{"id":"c2c5ef671b126fa8","type":"function","z":"55d2634b4497f676","name":"Chauffe-Eau OFF","func":"// Fonction qui ajoute un 0 devant un nombre pour compléter la chaine de caractères\nconst pad = n => {\n    return `${Math.floor(Math.abs(n))}`.padStart(2, '0');\n}\n// Fonction qui retourne la date au format 'dd/mm hh:ii'\nconst toDateString = date => {\n    return pad(date.getDate()) + '/' + pad(date.getMonth() + 1) + ' ' + pad(date.getHours()) + ':' + pad(date.getMinutes())\n}\n// Fonction qui retourne la date au format 'hh:ii'\nconst toTimeString = date => {\n    return pad(date.getHours()) + ':' + pad(date.getMinutes())\n}\n\nnode.status({fill:'grey',shape:\"dot\",text: toDateString(new Date()) + ' OFF'});\n\nlet msg1 = { topic: 'Chauffe-Eau', payload: 'off' };\n\nreturn msg1;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":800,"y":660,"wires":[["842b2a831be1d11a"]]},{"id":"ed0db56ed848c43c","type":"inject","z":"55d2634b4497f676","name":"06:01","props":[{"p":"payload"}],"repeat":"","crontab":"01 06 * * *","once":false,"onceDelay":0.1,"topic":"","payload":"off","payloadType":"str","x":180,"y":680,"wires":[["c2c5ef671b126fa8"]]},{"id":"109c93bf8ec11a63","type":"function","z":"55d2634b4497f676","name":"Chauffe-Eau ON","func":"// Fonction qui ajoute un 0 devant un nombre pour compléter la chaine de caractères\nconst pad = n => {\n    return `${Math.floor(Math.abs(n))}`.padStart(2, '0');\n}\n// Fonction qui retourne la date au format 'dd/mm hh:ii'\nconst toDateString = date => {\n    return pad(date.getDate()) + '/' + pad(date.getMonth() + 1) + ' ' + pad(date.getHours()) + ':' + pad(date.getMinutes())\n}\n// Fonction qui retourne la date au format 'hh:ii'\nconst toTimeString = date => {\n    return pad(date.getHours()) + ':' + pad(date.getMinutes())\n}\n\nnode.status({fill:'grey',shape:\"dot\",text: toDateString(new Date()) + ' ON'});\n\nlet msg1 = { topic: 'Chauffe-Eau', payload: 'on' };\n\nreturn msg1;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":800,"y":760,"wires":[["842b2a831be1d11a"]]},{"id":"842b2a831be1d11a","type":"switch","z":"55d2634b4497f676","name":"","property":"topic","propertyType":"msg","rules":[{"t":"eq","v":"Grid Set-Point","vt":"str"},{"t":"eq","v":"Minimum SoC","vt":"str"},{"t":"eq","v":"Active SoC Limit","vt":"str"},{"t":"eq","v":"ESS State","vt":"str"},{"t":"eq","v":"Chauffe-Eau","vt":"str"}],"checkall":"false","repair":false,"outputs":5,"x":1010,"y":480,"wires":[[],["7512ae02cce6f1b3"],[],["77cbb26718f1acb4"],["1560cde9be4c5de4"]]},{"id":"1560cde9be4c5de4","type":"mqtt out","z":"55d2634b4497f676","name":"Chauffe-Eau","topic":"shelly-01/command/switch:0","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"5e2b3fed23779bca","x":1170,"y":560,"wires":[]},{"id":"c9a93b995d633f92","type":"function","z":"55d2634b4497f676","name":"BatteryLife_Q","func":"// Fonction qui ajoute un 0 devant un nombre pour compléter la chaine de caractères\nconst pad = n => {\n    return `${Math.floor(Math.abs(n))}`.padStart(2, '0');\n}\n// Fonction qui retourne la date au format 'dd/mm hh:ii'\nconst toDateString = date => {\n    return pad(date.getDate()) + '/' + pad(date.getMonth() + 1) + ' ' + pad(date.getHours()) + ':' + pad(date.getMinutes())\n}\n// Fonction qui retourne la date au format 'hh:ii'\nconst toTimeString = date => {\n    return pad(date.getHours()) + ':' + pad(date.getMinutes())\n}\n\nlet error = false;\nlet error_msg = '';\n\n// Récupération des valeurs de l'algorithme BatteryLifeQ\nlet blq_default_soc_min = global.get('blq_default_soc_min');\nif (typeof blq_default_soc_min !== 'number' || blq_default_soc_min < 10) {\n    blq_default_soc_min = 10;\n    global.set('blq_default_soc_min', blq_default_soc_min);\n} else if (blq_default_soc_min > 85 ) {\n    blq_default_soc_min = 85;\n    global.set('blq_default_soc_min', blq_default_soc_min);\n}\n\nlet blq_active_soc_limit = global.get('blq_active_soc_limit');\nif (typeof blq_active_soc_limit !== 'number' || blq_active_soc_limit < blq_default_soc_min) {\n    blq_active_soc_limit = blq_default_soc_min;\n    global.set('blq_active_soc_limit', blq_active_soc_limit);\n} else if (blq_active_soc_limit > 85 ) {\n    blq_active_soc_limit = 85;\n    global.set('blq_active_soc_limit', blq_active_soc_limit);\n}\n\nlet blq_update_soc_limit = global.get('blq_update_soc_limit');\nif (typeof blq_update_soc_limit !== 'number' || blq_update_soc_limit < -20 || blq_update_soc_limit > 20 ) {\n    blq_update_soc_limit = 5;\n    global.set('blq_update_soc_limit', blq_update_soc_limit);\n}\n\n// Récupération de la valeur de l'état de charge actuel\nlet soc = msg.payload['SoC'];\nif (typeof soc !== 'number' || soc < 0 || soc > 100) {\n    error = true;\n    error_msg = 'Invalid soc: '+soc;\n    soc = 5\n}\n\nlet update_soc_limit = 5;\n\nif (soc == 100) {\n    update_soc_limit = -20\n} else if (soc >= 97) {\n    update_soc_limit = -5\n} else if (soc >= 85) {\n    update_soc_limit = 0\n}\n\nif (update_soc_limit < blq_update_soc_limit) {\n    blq_update_soc_limit = update_soc_limit\n    global.set('blq_update_soc_limit', blq_update_soc_limit);\n}\n\nlet fill, text;\n\nif (error) { \n    fill = 'red';\n    text = error_msg\n} else {\n    fill = 'grey';\n    text = 'Next update: '+ blq_update_soc_limit\n}\n\nnode.status({fill:fill,shape:'dot',text: toTimeString(new Date()) + ' ' + text});\n\nreturn null;","outputs":0,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1100,"y":240,"wires":[]},{"id":"b9e8707c664b7b4f","type":"link out","z":"55d2634b4497f676","name":"Data","mode":"link","links":["70e88d6b3f2d5d5a","26029c8f0fb69471","9f38db26f5558954"],"x":1035,"y":160,"wires":[]},{"id":"1dd136244f852102","type":"switch","z":"55d2634b4497f676","name":"Data Validation","property":"(\t   $type(payload.\"SoC\") = 'number'\t) and\t(\t   $type(payload.\"ESS State\") = 'number'\t) and\t(\t   $type(payload.\"PV Power\") = 'number'\t) and\t(\t   $type(payload.\"PV Energy\") = 'number'\t) and\t(\t   $type(payload.\"Grid Power\") = 'number'\t) and\t(\t   $type(payload.\"Battery Power\") = 'number'\t) and\t(\t   $type(payload.\"Critical Power\") = 'number'\t) and\t(\t   $type(payload.\"Normal Power\") = 'number'\t)","propertyType":"jsonata","rules":[{"t":"true"}],"checkall":"true","repair":false,"outputs":1,"x":900,"y":160,"wires":[["b9e8707c664b7b4f","c9a93b995d633f92"]]},{"id":"47394d539ef98e9a","type":"join","z":"55d2634b4497f676","name":"Join","mode":"custom","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"14.95","count":"9","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":710,"y":160,"wires":[["1dd136244f852102"]]},{"id":"0e57b37f80ee026a","type":"trigger","z":"55d2634b4497f676","name":"","op1":"","op2":"0","op1type":"pay","op2type":"str","duration":"0","extend":false,"overrideDelay":false,"units":"ms","reset":"","bytopic":"topic","topic":"topic","outputs":1,"x":540,"y":160,"wires":[["47394d539ef98e9a"]]},{"id":"b6c9b8011377c885","type":"inject","z":"55d2634b4497f676","name":"Every 1m","props":[{"p":"timestamp","v":"","vt":"date"}],"repeat":"60","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":130,"y":160,"wires":[["f381acf10717f3ed"]]},{"id":"f381acf10717f3ed","type":"function","z":"55d2634b4497f676","name":"Reset","func":"let date = new Date();\n\nreturn [[\n    { topic: 'Timestamp', reset: 1 },\n    { topic: 'SoC', reset: 1},\n    { topic: 'ESS State', reset: 1},\n    { topic: 'Grid Power', reset: 1},\n    { topic: 'Battery Power', reset: 1},\n    { topic: 'Critical Power', reset: 1},\n    { topic: 'Normal Power', reset: 1},\n    { topic: 'PV Power', reset: 1},\n    { topic: 'PV Energy', reset: 1},\n    { topic: 'Timestamp', payload: date.getTime() }\n],[\n    { topic: 'Timestamp', reset: 1 },\n    { topic: 'Production', reset: 1},\n    { topic: 'Import', reset: 1},\n    { topic: 'Export', reset: 1},\n    { topic: 'InToOut', reset: 1},\n    { topic: 'InToInv', reset: 1},\n    { topic: 'InvToIn', reset: 1},\n    { topic: 'InvToOut', reset: 1},\n    { topic: 'Timestamp', payload: date.getTime() }\n]];","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":310,"y":160,"wires":[["0e57b37f80ee026a"],[]]},{"id":"0cba01aa0ba20960","type":"victron-input-battery","z":"55d2634b4497f676","service":"com.victronenergy.battery/512","path":"/Soc","serviceObj":{"service":"com.victronenergy.battery/512","name":"Pylontech US5000"},"pathObj":{"path":"/Soc","type":"float","name":"State of charge (%)"},"name":"SoC","onlyChanges":false,"roundValues":"no","x":110,"y":240,"wires":[["0e57b37f80ee026a"]]},{"id":"7a085ab68ac9cb2e","type":"victron-input-ess","z":"55d2634b4497f676","service":"com.victronenergy.settings","path":"/Settings/CGwacs/BatteryLife/State","serviceObj":{"service":"com.victronenergy.settings","name":"Venus settings"},"pathObj":{"path":"/Settings/CGwacs/BatteryLife/State","type":"enum","name":"ESS state","enum":{"1":"BatteryLife enabled (GUI controlled)","2":"Optimized Mode /w BatteryLife: self consumption","3":"Optimized Mode /w BatteryLife: self consumption, SoC exceeds 85%","4":"Optimized Mode /w BatteryLife: self consumption, SoC at 100%","5":"Optimized Mode /w BatteryLife: SoC below dynamic SoC limit","6":"Optimized Mode /w BatteryLife: SoC has been below SoC limit for more than 24 hours. Charging the battery (5A)","7":"Optimized Mode /w BatteryLife: Inverter/Charger is in sustain mode","8":"Optimized Mode /w BatteryLife: recharging, SoC dropped by 5% or more below the minimum SoC","9":"'Keep batteries charged' mode is enabled","10":"Optimized mode w/o BatteryLife: self consumption, SoC at or above minimum SoC","11":"Optimized mode w/o BatteryLife: self consumption, SoC is below minimum SoC","12":"Optimized mode w/o BatteryLife: recharging, SoC dropped by 5% or more below minimum SoC"}},"initial":"","name":"ESS State","onlyChanges":false,"x":120,"y":280,"wires":[["0e57b37f80ee026a"]]},{"id":"c2c5cad032487347","type":"victron-input-pvinverter","z":"55d2634b4497f676","service":"com.victronenergy.pvinverter/32","path":"/Ac/L1/Power","serviceObj":{"service":"com.victronenergy.pvinverter/32","name":"Enphase IQ8"},"pathObj":{"path":"/Ac/L1/Power","type":"float","name":"L1 Power (W)"},"name":"PV Power","onlyChanges":false,"roundValues":"3","x":120,"y":320,"wires":[["f77a02bc042302e4","0e57b37f80ee026a"]]},{"id":"557ffcc0ce4aa702","type":"victron-input-gridmeter","z":"55d2634b4497f676","service":"com.victronenergy.grid/30","path":"/Ac/Power","serviceObj":{"service":"com.victronenergy.grid/30","name":"Réseau ENEDIS"},"pathObj":{"path":"/Ac/Power","type":"float","name":"Power (W)"},"name":"Grid Power","onlyChanges":false,"roundValues":"1","x":120,"y":360,"wires":[["0e57b37f80ee026a"]]},{"id":"0a285a0c79fecacf","type":"victron-input-battery","z":"55d2634b4497f676","service":"com.victronenergy.battery/512","path":"/Dc/0/Power","serviceObj":{"service":"com.victronenergy.battery/512","name":"Pylontech US5000"},"pathObj":{"path":"/Dc/0/Power","type":"float","name":"Battery power (W)"},"name":"Battery Power","onlyChanges":false,"roundValues":"no","x":130,"y":400,"wires":[["0e57b37f80ee026a"]]},{"id":"b72e4039776c2e55","type":"victron-input-custom","z":"55d2634b4497f676","service":"com.victronenergy.system/0","path":"/Ac/ConsumptionOnInput/L1/Power","serviceObj":{"service":"com.victronenergy.system/0","name":"com.victronenergy.system (0)"},"pathObj":{"path":"/Ac/ConsumptionOnInput/L1/Power","name":"/Ac/ConsumptionOnInput/L1/Power","type":"number","value":0},"name":"Normal Power","onlyChanges":false,"roundValues":"1","x":130,"y":440,"wires":[["0e57b37f80ee026a"]]},{"id":"30097b775dd0a7a8","type":"victron-input-custom","z":"55d2634b4497f676","service":"com.victronenergy.system/0","path":"/Ac/ConsumptionOnOutput/L1/Power","serviceObj":{"service":"com.victronenergy.system/0","name":"com.victronenergy.system (0)"},"pathObj":{"path":"/Ac/ConsumptionOnOutput/L1/Power","name":"/Ac/ConsumptionOnOutput/L1/Power","type":"number","value":294},"name":"Critical Power","onlyChanges":false,"roundValues":"1","x":130,"y":480,"wires":[["0e57b37f80ee026a"]]},{"id":"f77a02bc042302e4","type":"function","z":"55d2634b4497f676","name":"PV Energy","func":"let now = (new Date()).getTime();\nlet error = false;\nlet fill = 'grey';\n\nlet power = msg.payload;\nif (typeof power !== 'number') {\n    error = true;\n    text = 'Invalid payload: ' + power;\n}\n\nlet timestamp = flow.get(\"victron_prod_jr_timestamp\");\nif (typeof timestamp !== 'number' || timestamp >= now) {\n    error = true;\n    flow.set('victron_prod_jr_timestamp', now);\n    text = 'Invalid timestamp: ' + timestamp;\n}\n\nlet prod_jr = flow.get(\"victron_prod_jr\");\nif (typeof prod_jr !== 'number') {\n    error = true;\n    flow.set('victron_prod_jr', 0);\n    text = 'Invalid prod: ' + prod_jr;\n}\n\nlet msgs = [];\n\nif (error) {\n    fill = 'red';\n} else {\n    let delta_time = (now - timestamp)/1000; // nombre de secondes\n    if (delta_time > 15) delta_time = 15;    // limiter à 15 secondes\n\n    prod_jr += power * delta_time / 3600;\n    flow.set(\"victron_prod_jr\", prod_jr);\n\n    flow.set('victron_prod_jr_timestamp', now);\n    \n    msgs.push({ topic: 'PV Energy', payload: Math.round(prod_jr)/1000 });\n\n    text = msgs[0].payload.toFixed(3);\n}\n\nnode.status({fill:fill,shape:'dot',text:text});\n\nreturn [msgs];","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":330,"y":340,"wires":[["0e57b37f80ee026a"]]},{"id":"9f38db26f5558954","type":"link in","z":"55d2634b4497f676","name":"link in 3","links":["b9e8707c664b7b4f"],"x":245,"y":520,"wires":[["dabd14eee03bc774","f198134add478f22"]]},{"id":"aae819d2a6844aa3","type":"comment","z":"55d2634b4497f676","name":"Collecter les données locales et les valider","info":"","x":200,"y":120,"wires":[]},{"id":"c238852968ce925d","type":"mqtt in","z":"55d2634b4497f676","name":"Chauffe-Eau Status","topic":"shelly-01/status/switch:0","qos":"2","datatype":"auto-detect","broker":"5e2b3fed23779bca","nl":false,"rap":true,"rh":0,"inputs":0,"x":130,"y":80,"wires":[["ab7888d2a30816f4"]]},{"id":"ab7888d2a30816f4","type":"function","z":"55d2634b4497f676","name":"Set flow variable","func":"let text, fill;\n\nif (typeof(msg.payload.output) === 'boolean') {\n    flow.set('shelly_1_closed', msg.payload.output);\n    text = (msg.payload.output) ? 'Closed' : 'Open';\n    fill = 'grey';\n} else {\n    flow.set('shelly_1_closed', null);\n    text = 'Invalid value: '+msg.payload.output;\n    fill = 'red';\n}\n\nnode.status({fill:fill,shape:'dot',text:text});\n\nreturn null;","outputs":0,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":360,"y":80,"wires":[]},{"id":"21018081643a8bf1","type":"comment","z":"55d2634b4497f676","name":"Collecter l'état des relais du contacteur Shelly","info":"","x":210,"y":40,"wires":[]},{"id":"a8263dff501415be","type":"function","z":"55d2634b4497f676","name":"Chauffe-Eau HC","func":"// Fonction qui ajoute un 0 devant un nombre pour compléter la chaine de caractères\nconst pad = n => {\n    return `${Math.floor(Math.abs(n))}`.padStart(2, '0');\n}\n// Fonction qui retourne la date au format 'dd/mm hh:ii'\nconst toDateString = date => {\n    return pad(date.getDate()) + '/' + pad(date.getMonth() + 1) + ' ' + pad(date.getHours()) + ':' + pad(date.getMinutes())\n}\n// Fonction qui retourne la date au format 'hh:ii'\nconst toTimeString = date => {\n    return pad(date.getHours()) + ':' + pad(date.getMinutes())\n}\n\nlet date = new Date();\nlet timestamp = date.getTime();\n\nlet text = '';\nlet fill = 'grey';\n\nlet error = false;\n\n// Récupération de la valeur de l'état de charge\nlet soc = msg.payload['SoC'];\nif (typeof soc !== 'number' || soc < 5 || soc > 100) {\n    error = true;\n    text = 'Invalid soc: ' + soc;\n    soc = 5;\n}\n\n// Récupération de la limite active de l'algorithme BatteryLifeQ\nlet active_soc_limit = global.get('blq_active_soc_limit');\nif (typeof(active_soc_limit) !== 'number' || active_soc_limit < 5 || active_soc_limit > 85) {\n    error = true;\n    text = 'Invalid active_soc_limit: ' + active_soc_limit;\n    active_soc_limit = (soc >= 5 && soc <= 50) ? soc : (soc > 50) ? 50 : 10;\n    global.set('active_soc_limit', active_soc_limit);\n}\n\n// Récupération de l'état du contacteur contrôlant le chauffe-eau\nlet shelly_1_closed = flow.get('shelly_1_closed');\nif (typeof shelly_1_closed !== 'boolean') {\n    error = true;\n    text = 'Invalid shelly_1_closed: ' + shelly_1_closed;\n    shelly_1_closed = true;\n    flow.set('shelly_1_status', shelly_1_closed);\n}\n\n// Récupération de l'horodatage de la dernière chauffe complète\nlet ce_full_timestamp = flow.get('ce_full_timestamp');\nif (typeof ce_full_timestamp !== 'number' || ce_full_timestamp > timestamp) {\n    error = true;\n    text = 'Invalid ce_full_timestamp: ' + ce_full_timestamp;\n    ce_full_timestamp = timestamp - 52*60*60*1000; // 52h avant maintenant\n    flow.set('ce_full_timestamp', ce_full_timestamp);\n}\n\n// Récupération de la valeurs de consommation des charges non-secourues\nlet normal_load_w = msg.payload['Normal Power'];\nif (typeof normal_load_w !== 'number' || normal_load_w < -1000 || normal_load_w > 10000) {\n    error = true;\n    text = 'Invalid normal_load_w: '+normal_load_w;\n    normal_load_w = 0;\n}\n\nlet msgs = [];\n\nif (error) {\n    // Eteindre le chauffe-eau\n    fill = 'red';\n    msgs.push({ topic: 'Chauffe-Eau', payload: 'off' });\n} else if (timestamp - ce_full_timestamp < 52*60*60*1000) {\n    // La dernière chauffe date de moins de 52h -> Garder le chauffe-eau éteint.\n    msgs.push({ topic: 'Chauffe-Eau', payload: 'off' });\n    \n    // Afficher la valeur de la dernière chauffe complète\n    date.setTime(ce_full_timestamp);\n    text = 'Full ' + toDateString(date);\n} else {\n    if (shelly_1_closed) {\n        // Relais est fermé -> Chauffe-eau allumé\n        \n        if (normal_load_w < 1600) {\n            // L'eau a fini de chauffer\n            \n            // Eteindre le chauffe-eau et restaurer les valeurs par défaut.\n            msgs.push({ topic: 'Chauffe-Eau', payload: 'off' });\n            msgs.push({ topic: 'Minimum SoC', payload: active_soc_limit });\n            msgs.push({ topic: 'ESS State', payload: 10 });\n            \n            // Actualiser la valeur de la dernière chauffe complète\n            flow.set('ce_full_timestamp', timestamp);\n            text = 'Full ' + toDateString(date);\n        } else {\n            // L'eau est en train de chauffer\n            \n            // Maintenir le chauffe-eau allumé et empêcher de chauffer l'eau à partir des batteries sauf si SoC >= 85.\n            text = 'Heating';\n            msgs.push({ topic: 'Chauffe-Eau', payload: 'on' });\n            msgs.push({ topic: 'Minimum SoC', payload: (soc < active_soc_limit) ? active_soc_limit : (soc < 85) ? soc+1 : 85 });\n            msgs.push({ topic: 'ESS State', payload: 10 });\n        }\n    } else {\n        // Relais est ouvert -> Chauffe-eau éteint\n        \n        // Allumer le chauffe-eau et empêcher de chauffer l'eau à partir des batteries sauf si SoC >= 85.\n        text = 'Turn on';\n        msgs.push({ topic: 'Chauffe-Eau', payload: 'on' });\n        msgs.push({ topic: 'Minimum SoC', payload: (soc < active_soc_limit) ? active_soc_limit : (soc < 85) ? soc+1 : 85 });\n        msgs.push({ topic: 'ESS State', payload: 10 });\n    }\n}\n\nnode.status({fill:fill,shape:\"dot\",text: toTimeString(new Date()) + ' ' + text});\n\nreturn msgs;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":790,"y":720,"wires":[["842b2a831be1d11a"]]},{"id":"7512ae02cce6f1b3","type":"victron-output-ess","z":"55d2634b4497f676","service":"com.victronenergy.settings","path":"/Settings/CGwacs/BatteryLife/MinimumSocLimit","serviceObj":{"service":"com.victronenergy.settings","name":"Venus settings"},"pathObj":{"path":"/Settings/CGwacs/BatteryLife/MinimumSocLimit","type":"integer","name":"Minimum Discharge SOC (%)","writable":true},"name":"Minimum SoC","onlyChanges":false,"x":1180,"y":440,"wires":[]},{"id":"77cbb26718f1acb4","type":"victron-output-ess","z":"55d2634b4497f676","service":"com.victronenergy.settings","path":"/Settings/CGwacs/BatteryLife/State","serviceObj":{"service":"com.victronenergy.settings","name":"Venus settings"},"pathObj":{"path":"/Settings/CGwacs/BatteryLife/State","type":"enum","name":"ESS state","enum":{"1":"BatteryLife enabled (GUI controlled)","2":"Optimized Mode /w BatteryLife: self consumption","3":"Optimized Mode /w BatteryLife: self consumption, SoC exceeds 85%","4":"Optimized Mode /w BatteryLife: self consumption, SoC at 100%","5":"Optimized Mode /w BatteryLife: SoC below dynamic SoC limit","6":"Optimized Mode /w BatteryLife: SoC has been below SoC limit for more than 24 hours. Charging the battery (5A)","7":"Optimized Mode /w BatteryLife: Inverter/Charger is in sustain mode","8":"Optimized Mode /w BatteryLife: recharging, SoC dropped by 5% or more below the minimum SoC","9":"'Keep batteries charged' mode is enabled","10":"Optimized mode w/o BatteryLife: self consumption, SoC at or above minimum SoC","11":"Optimized mode w/o BatteryLife: self consumption, SoC is below minimum SoC","12":"Optimized mode w/o BatteryLife: recharging, SoC dropped by 5% or more below minimum SoC"},"writable":true},"initial":"","name":"ESS State","onlyChanges":false,"x":1170,"y":520,"wires":[]},{"id":"5e2b3fed23779bca","type":"mqtt-broker","name":"MQTT LAN","broker":"192.168.219.42","port":"1883","clientid":"nodered-victron","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthRetain":"false","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closeRetain":"false","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willRetain":"false","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""}]

2 Likes

Merci @Q.x :grinning:

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.