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":""}]