2026-06-05, 06:05 AM
(This post was last modified: 2026-06-05, 06:08 AM by SCarns.
Edit Reason: typos
)
Two years ago, well into my Openplotter integration, I bought a new boat and started from scratch with OP on a Pi5. Having had fun on the previous boat with Ruvvi Tags and Shelly relays, I dove in with this boat with the previously posted lighting automation. I discovered the thermostat on the refrigerator unit was bad. It's an old unit from Denmark and Im in the US. Rather than trying to mess around trying to find a new mechanical thermostat, I went ahead and installed a shelly relay to close the thermostat switch to activate the compressor (the rest of the system is fine) and had a Ruuvi Tag in the box to sense the temperature. SignalK and Node_Red do the rest.
It was easy enough and worked well as it was. Then I had the idea to hand it over to ChatGPT and it has helped me build a much more robust system with safety checks, delayed start ups, clean initialization, and room for expansion (defrost cycles, door open events, etc.). Refrigeration V2.0 is up and running. If currently also drives a variable speed circulation fan (run by a shelly RGBW) with room for a defrost fan and cooling lift fan I plan to install soon. The circulation fan does wonders at keeping the whole box even, reducing load on the compressor, ice build up on the evaporator, etc. This uses 1 ruuvi tag and two Shellys - one on/off for the compressor and 1 dimmable for the fan(s). There is a simple "config" node for constant setting such as temperature set point, fan speeds, etc. in an easy to access place.
[{"id":"1bf00d9c9a0613bd","type":"tab","label":"Refrigerator V2","disabled":false,"info":"","env":[]},{"id":"49bac916bcf7bd87","type":"signalk-subscribe","z":"1bf00d9c9a0613bd","name":"Refrigerator Temperature","mode":"sendAll","flatten":true,"context":"vessels.self","path":"environment.inside.refrigerator.temperature","source":"","period":1000,"x":150,"y":180,"wires":[["85ca6a094da20e80"]]},{"id":"68036bea9ccc7429","type":"function","z":"1bf00d9c9a0613bd","name":"Pole Compressor Relay","func":"msg.payload = {\n method: \"Switch.GetStatus\",\n parameters: {\n id: 0\n }\n};\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":330,"y":260,"wires":[["6b136983817d7bdd"]]},{"id":"6b136983817d7bdd","type":"shelly-gen2","z":"1bf00d9c9a0613bd","hostname":"172.16.1.110","description":"Compressor","mode":"polling","verbose":false,"server":"","outputmode":"event","uploadretryinterval":5000,"pollinginterval":5000,"pollstatus":false,"getstatusoncommand":true,"devicetype":"Relay","devicetypemustmatchexactly":false,"captureblutooth":false,"outputs":1,"x":170,"y":320,"wires":[["2a355efa79f69d1e"]]},{"id":"2a355efa79f69d1e","type":"function","z":"1bf00d9c9a0613bd","name":"Pole Result","func":"msg.payload = Number(msg.payload.switch0.output);\nmsg.topic = \"compressor\";\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":370,"y":320,"wires":[["6e5eebe2e9fb6524"]]},{"id":"85ca6a094da20e80","type":"change","z":"1bf00d9c9a0613bd","name":"topic","rules":[{"t":"set","p":"topic","pt":"msg","to":"temp","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":430,"y":180,"wires":[["0b72854cc33bc6b1"]]},{"id":"0b72854cc33bc6b1","type":"function","z":"1bf00d9c9a0613bd","name":"State Cache","func":"let state = context.get(\"state\") || {};\n\nstate[msg.topic] = msg.payload;\nif (msg.topic === \"constants\") {\n state.constants = msg.payload;\n}\n\n// ----------------------------------------\n// NORMALIZE TYPES\n// ----------------------------------------\n\nif (state.temp !== undefined) {\n state.temp = Number(state.temp);\n}\n\nif (state.setpoint !== undefined) {\n state.setpoint = Number(state.setpoint);\n}\n\nif (state.compressor !== undefined) {\n state.compressor = Number(state.compressor);\n}\n\nif (state.compressorRunSeconds !== undefined) {\n state.compressorRunSeconds =\n Number(state.compressorRunSeconds);\n}\n\nif (state.compressorOffSeconds !== undefined) {\n state.compressorOffSeconds =\n Number(state.compressorOffSeconds);\n}\n\nif (state.systemEnabled !== undefined) {\n state.systemEnabled =\n Number(state.systemEnabled) === 1;\n}\n\ncontext.set(\"state\", state);\n\nmsg.payload = state;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":770,"y":260,"wires":[["9b381e1898b746b9"]]},{"id":"74d9cd071890e077","type":"inject","z":"1bf00d9c9a0613bd","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"60","crontab":"","once":true,"onceDelay":"5","topic":"","payload":"","payloadType":"date","x":110,"y":40,"wires":[["7a699e4f8437c853","2e53cfb5c781239e"]]},{"id":"7a699e4f8437c853","type":"change","z":"1bf00d9c9a0613bd","name":"setpoint","rules":[{"t":"set","p":"payload","pt":"msg","to":"277.59","tot":"num"},{"t":"set","p":"topic","pt":"msg","to":"setpoint","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":320,"y":40,"wires":[["0b72854cc33bc6b1","cf8f11130e2d4dfd"]]},{"id":"cf8f11130e2d4dfd","type":"change","z":"1bf00d9c9a0613bd","name":"to SK","rules":[{"t":"set","p":"topic","pt":"msg","to":"environment.inside.refrigerator.setting","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":530,"y":40,"wires":[["a0c7a1f4eccdd3fe"]]},{"id":"a0c7a1f4eccdd3fe","type":"signalk-send-pathvalue","z":"1bf00d9c9a0613bd","name":"","path":"","source":"","meta":"","x":810,"y":40,"wires":[]},{"id":"9b381e1898b746b9","type":"function","z":"1bf00d9c9a0613bd","name":"Timer","func":"let s = msg.payload;\n\n// Current compressor state\nlet compressor = Number(s.compressor || 0);\n\n// Previous state\nlet lastCompressor = context.get(\"lastCompressor\");\nif (lastCompressor === undefined) {\n lastCompressor = compressor;\n}\n\n// Timers\nlet compressorRunSeconds =\n context.get(\"compressorRunSeconds\") || 0;\n\nlet compressorOffSeconds =\n context.get(\"compressorOffSeconds\") || 0;\n\n// Poll interval\nconst POLL_INTERVAL = 5;\n\n// State machine\nif (compressor === 1) {\n\n compressorRunSeconds += POLL_INTERVAL;\n compressorOffSeconds = 0;\n\n} else {\n\n compressorOffSeconds += POLL_INTERVAL;\n compressorRunSeconds = 0;\n\n}\n\n// Save state\ncontext.set(\"compressorRunSeconds\", compressorRunSeconds);\ncontext.set(\"compressorOffSeconds\", compressorOffSeconds);\ncontext.set(\"lastCompressor\", compressor);\n\n// Add to payload\ns.compressorRunSeconds = compressorRunSeconds;\ns.compressorOffSeconds = compressorOffSeconds;\n\nmsg.payload = s;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":130,"y":380,"wires":[["228f6e46c43c46b3"]]},{"id":"c89f0a81a5ea1717","type":"function","z":"1bf00d9c9a0613bd","name":"Compressor Controller","func":"let s = msg.payload;\n\n// ----------------------------------------\n// SETTINGS\n// ----------------------------------------\n\nconst MIN_OFF_TIME =\n s.constants.MIN_OFF_TIME;\n\nconst MIN_RUN_TIME =\n s.constants.MIN_RUN_TIME;\n\nlet compressorCommand = null;\n\nif (!s.systemEnabled) {\n\n msg.payload = {\n compressor: 0\n };\n\n return msg;\n}\n\n// ----------------------------------------\n// START COMPRESSOR\n// ----------------------------------------\n\nif (\n !s.coolingActive &&\n s.coolingDemand &&\n s.compressorOffSeconds >= MIN_OFF_TIME\n) {\n compressorCommand = 1;\n}\n\n// ----------------------------------------\n// STOP COMPRESSOR\n// ----------------------------------------\n\nelse if (\n s.coolingActive &&\n s.atSetpoint &&\n s.compressorRunSeconds >= MIN_RUN_TIME\n) {\n compressorCommand = 0;\n}\n\n// ----------------------------------------\n// NO CHANGE\n// ----------------------------------------\n\nif (compressorCommand === null) {\n return null;\n}\n\n// ----------------------------------------\n// OUTPUT\n// ----------------------------------------\n\nmsg.payload = {\n compressor: compressorCommand\n};\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":160,"y":660,"wires":[["279c40bcb0457f54"]]},{"id":"279c40bcb0457f54","type":"function","z":"1bf00d9c9a0613bd","name":"Shelly Control","func":"if (msg.payload.compressor === 1) {\n\n msg.payload = {\n method: \"Switch.Set\",\n parameters: {\n id: 0,\n on: true\n }\n };\n\n return msg;\n}\n\nif (msg.payload.compressor === 0) {\n\n msg.payload = {\n method: \"Switch.Set\",\n parameters: {\n id: 0,\n on: false\n }\n };\n\n return msg;\n}\n\nreturn null;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":400,"y":660,"wires":[["aba6b38a4343b153"]]},{"id":"228f6e46c43c46b3","type":"function","z":"1bf00d9c9a0613bd","name":"Refrigeration State","func":"let s = msg.payload;\n\n// ----------------------------------------\n// TEMPERATURE STATE\n// ----------------------------------------\n\nconst START_DIFF =\n s.constants.START_DIFF;\n\ns.coolingDemand =\n s.temp >= (s.setpoint + START_DIFF);\n\ns.atSetpoint =\n s.temp <= s.setpoint;\n\n// ----------------------------------------\n// COMPRESSOR STATE\n// ----------------------------------------\n\ns.coolingActive =\n s.compressor === 1;\n\n// ----------------------------------------\n// FUTURE STATES\n// ----------------------------------------\n\n// placeholders for future expansion\n\ns.defrostActive = false;\n\ns.coolLiftActive = false;\n\n// ----------------------------------------\n// OUTPUT\n// ----------------------------------------\n\nmsg.payload = s;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":310,"y":380,"wires":[["3f62a5848505c5ea"]]},{"id":"3f62a5848505c5ea","type":"function","z":"1bf00d9c9a0613bd","name":"Mode Manager","func":"let s = msg.payload;\n\n// ----------------------------------------\n// SYSTEM OFF\n// ----------------------------------------\n\nif (!s.systemEnabled) {\n\n s.mode = \"OFF\";\n}\n\n// ----------------------------------------\n// COOLING\n// ----------------------------------------\n\nelse if (s.coolingActive) {\n\n s.mode = \"COOLING\";\n}\n\n// ----------------------------------------\n// PULLDOWN\n// ----------------------------------------\n\nelse if (s.coolingDemand) {\n\n s.mode = \"PULLDOWN\";\n}\n\n// ----------------------------------------\n// AT SETPOINT\n// ----------------------------------------\n\nelse {\n\n s.mode = \"AT_SETPOINT\";\n}\n\nmsg.payload = s;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":180,"y":440,"wires":[["c89f0a81a5ea1717","3e51e306ca707f44","1f818feaf71ec23f","c84ffb40f6bbafc4"]]},{"id":"89c2d075dec204bd","type":"signalk-send-pathvalue","z":"1bf00d9c9a0613bd","name":"Status","path":"environment.inside.refrigerator.mode","source":"","meta":"","x":590,"y":520,"wires":[]},{"id":"3e51e306ca707f44","type":"function","z":"1bf00d9c9a0613bd","name":"Status Publisher","func":"let s = msg.payload;\n\nlet status = \"\";\n\nswitch (s.mode) {\n\n case \"OFF\":\n status = \"Off\";\n break;\n\n case \"AT_SETPOINT\":\n status = \"At Setpoint\";\n break;\n\n case \"PULLDOWN\":\n status = \"Pulling Down\";\n break;\n\n case \"COOLING\":\n status = \"Cooling\";\n break;\n\n case \"DEFROST\":\n status = \"Defrost\";\n break;\n\n case \"COOL_LIFT\":\n status = \"Cool Lift\";\n break;\n\n default:\n status = \"Unknown\";\n}\n\nmsg.payload = status;\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":400,"y":500,"wires":[["89c2d075dec204bd"]]},{"id":"1f818feaf71ec23f","type":"function","z":"1bf00d9c9a0613bd","name":"Circulation Fan Controller","func":"let s = msg.payload;\n\n// ----------------------------------------\n// FAN SPEED SETTINGS\n//\n// Current blower is noisy below 70%.\n// Future PWM fan:\n//\n// OFF = 0\n// SETPOINT = 30\n// PULLDOWN = 70\n// COOLING = 85\n//\n// ----------------------------------------\n\nconst FAN_OFF =\n s.constants.FAN_OFF;\n\nconst FAN_SETPOINT =\n s.constants.FAN_SETPOINT;\n\nconst FAN_PULLDOWN =\n s.constants.FAN_PULLDOWN;\n\nconst FAN_COOLING =\n s.constants.FAN_COOLING;\n\n// ----------------------------------------\n// MODE LOGIC\n// ----------------------------------------\n\nlet fanSpeed = FAN_OFF;\n\nif (!s.systemEnabled) {\n fanSpeed = FAN_OFF;\n}\n\nelse if (s.mode === \"AT_SETPOINT\") {\n fanSpeed = FAN_SETPOINT;\n}\n\nelse if (s.mode === \"PULLDOWN\") {\n fanSpeed = FAN_PULLDOWN;\n}\n\nelse if (s.mode === \"COOLING\") {\n fanSpeed = FAN_COOLING;\n}\n\n// ----------------------------------------\n// OUTPUT\n// ----------------------------------------\n\nmsg.payload = fanSpeed;\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":170,"y":540,"wires":[["b8ee811fe694a2c3"]]},{"id":"b8ee811fe694a2c3","type":"function","z":"1bf00d9c9a0613bd","name":"Fan Output Mapper","func":"let speed = Number(msg.payload || 0);\n\nmsg.payload = {\n light: 0,\n brightness: speed,\n timer: 0,\n on: speed > 0\n};\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":410,"y":540,"wires":[["1002e82ffa540a09"]]},{"id":"5555bac5cf3bc343","type":"change","z":"1bf00d9c9a0613bd","name":"topic","rules":[{"t":"set","p":"topic","pt":"msg","to":"electrical.switches.fridgeSwitch.state","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":590,"y":140,"wires":[["dcd1a106a9382490"]]},{"id":"dcd1a106a9382490","type":"signalk-send-pathvalue","z":"1bf00d9c9a0613bd","name":"","path":"","source":"","meta":"","x":810,"y":140,"wires":[]},{"id":"c84ffb40f6bbafc4","type":"function","z":"1bf00d9c9a0613bd","name":"State Publisher","func":"let s = msg.payload;\n\n// Output 1\nmsg.payload = s.coolingDemand;\nmsg.topic = \"environment.inside.refrigerator.coolingDemand\";\n\n// Output 2\nlet msg2 = {\n topic: \"environment.inside.refrigerator.coolingActive\",\n payload: s.coolingActive\n};\n\n// Output 3\nlet msg3 = {\n topic: \"environment.inside.refrigerator.compressorRunSeconds\",\n payload: s.compressorRunSeconds\n};\n\n// Output 4\nlet msg4 = {\n topic: \"environment.inside.refrigerator.compressorOffSeconds\",\n payload: s.compressorOffSeconds\n};\n\nlet msg5 = {\n\n topic: \"environment.inside.refrigerator.atSetpoint\",\n\n payload: s.atSetpoint\n\n};\n\nreturn [msg, msg2, msg3, msg4, msg5];","outputs":5,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":560,"y":440,"wires":[["3ddcb2d94ed3b3d3"],["ac09af3f974d82ed"],["e884104019d5ad9b"],["806f69a1e74bacef"],["ec4964c669ff8193"]]},{"id":"3ddcb2d94ed3b3d3","type":"signalk-send-pathvalue","z":"1bf00d9c9a0613bd","name":"coolingActive","path":"environment.inside.refrigerator.coolingActive","source":"","meta":"","x":840,"y":340,"wires":[]},{"id":"ac09af3f974d82ed","type":"signalk-send-pathvalue","z":"1bf00d9c9a0613bd","name":"coolingDemand","path":"environment.inside.refrigerator.coolingDemand","source":"","meta":"","x":840,"y":400,"wires":[]},{"id":"e884104019d5ad9b","type":"signalk-send-pathvalue","z":"1bf00d9c9a0613bd","name":"compressorRunSeconds","path":"environment.inside.refrigerator.compressorRunSeconds","source":"","meta":"","x":810,"y":460,"wires":[]},{"id":"806f69a1e74bacef","type":"signalk-send-pathvalue","z":"1bf00d9c9a0613bd","name":"compressorOffSeconds","path":"environment.inside.refrigerator.compressorOffSeconds","source":"","meta":"","x":810,"y":520,"wires":[]},{"id":"6eafb2d03cf63f01","type":"signalk-send-pathvalue","z":"1bf00d9c9a0613bd","name":"","path":"","source":"","meta":"","x":810,"y":640,"wires":[]},{"id":"1002e82ffa540a09","type":"shelly-gen1","z":"1bf00d9c9a0613bd","hostname":"172.16.1.105","description":"FridgeFans","mode":"polling","verbose":false,"server":"","outputmode":"event","uploadretryinterval":5000,"pollinginterval":5000,"pollstatus":false,"getstatusoncommand":true,"devicetype":"RGBW","devicetypemustmatchexactly":false,"outputs":1,"x":390,"y":600,"wires":[["2bea9a0980918f11"]]},{"id":"2bea9a0980918f11","type":"function","z":"1bf00d9c9a0613bd","name":"Fan Speed","func":"msg.payload = msg.payload.lights[0].brightness;\nmsg.payload = Number(msg.payload);\nmsg.payload = msg.payload / 100;\nmsg.topic = 'electrical.switches.refrigeratorCircFan.speed';\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":570,"y":600,"wires":[["6eafb2d03cf63f01"]]},{"id":"aba6b38a4343b153","type":"shelly-gen2","z":"1bf00d9c9a0613bd","hostname":"172.16.1.110","description":"On/Off","mode":"polling","verbose":false,"server":"","outputmode":"event","uploadretryinterval":5000,"pollinginterval":5000,"pollstatus":false,"getstatusoncommand":true,"devicetype":"Relay","devicetypemustmatchexactly":false,"captureblutooth":false,"outputs":1,"x":590,"y":660,"wires":[["b2f551245ced9c3c"]]},{"id":"b2f551245ced9c3c","type":"switch","z":"1bf00d9c9a0613bd","name":"","property":"payload.switch0.output","propertyType":"msg","rules":[{"t":"false"},{"t":"true"}],"checkall":"true","repair":false,"outputs":2,"x":370,"y":740,"wires":[["b8bb04c4b6f12522"],["5a97b03bc3061b8a"]]},{"id":"b8bb04c4b6f12522","type":"change","z":"1bf00d9c9a0613bd","name":"Off","rules":[{"t":"set","p":"payload","pt":"msg","to":"Off","tot":"str"},{"t":"set","p":"topic","pt":"msg","to":"electrical.switches.refrigeratorCompressor.status","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":590,"y":720,"wires":[["33e4aa3179dd5dba"]]},{"id":"5a97b03bc3061b8a","type":"change","z":"1bf00d9c9a0613bd","name":"On","rules":[{"t":"set","p":"payload","pt":"msg","to":"Running","tot":"str"},{"t":"set","p":"topic","pt":"msg","to":"electrical.switches.refrigeratorCompressor.status","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":590,"y":760,"wires":[["33e4aa3179dd5dba"]]},{"id":"33e4aa3179dd5dba","type":"signalk-send-pathvalue","z":"1bf00d9c9a0613bd","name":"","path":"","source":"","meta":"","x":810,"y":740,"wires":[]},{"id":"d385a50b4b7c73aa","type":"signalk-put-handler","z":"1bf00d9c9a0613bd","name":"","path":"electrical.switches.fridgeSwitch.state","pending":false,"x":130,"y":140,"wires":[["1902dd6903e7ffe2"]]},{"id":"1902dd6903e7ffe2","type":"change","z":"1bf00d9c9a0613bd","name":"topic","rules":[{"t":"set","p":"topic","pt":"msg","to":"systemEnabled","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":430,"y":140,"wires":[["5555bac5cf3bc343","0b72854cc33bc6b1"]]},{"id":"6e5eebe2e9fb6524","type":"function","z":"1bf00d9c9a0613bd","name":"Startup Mgr","func":"let initialized = context.get(\"initialized\") || false;\n\n// only run once after deploy\n\nif (!initialized) {\n let compressor = Number(msg.payload);\n if (compressor === 1) {\n msg.compressorRunSeconds = 180;\n msg.compressorOffSeconds = 0;\n } else {\n msg.compressorRunSeconds = 0;\n msg.compressorOffSeconds = 300;\n }\n context.set(\"initialized\", true);\n}\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":550,"y":320,"wires":[["0b72854cc33bc6b1"]]},{"id":"3676a22c1ffe4a8c","type":"signalk-get","z":"1bf00d9c9a0613bd","name":"Get Swicth","path":"electrical.switches.fridgeSwitch.state","x":290,"y":220,"wires":[["ff210961e6a83fc0"],[]]},{"id":"4789d200a1c739ac","type":"inject","z":"1bf00d9c9a0613bd","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"5","crontab":"","once":true,"onceDelay":"5","topic":"","payload":"","payloadType":"date","x":110,"y":240,"wires":[["68036bea9ccc7429","3676a22c1ffe4a8c"]]},{"id":"ec4964c669ff8193","type":"signalk-send-pathvalue","z":"1bf00d9c9a0613bd","d":true,"name":"","path":"","source":"","meta":"","x":810,"y":580,"wires":[]},{"id":"ff210961e6a83fc0","type":"change","z":"1bf00d9c9a0613bd","name":"topic","rules":[{"t":"set","p":"topic","pt":"msg","to":"systemEnabled","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":550,"y":220,"wires":[["0b72854cc33bc6b1"]]},{"id":"2e53cfb5c781239e","type":"function","z":"1bf00d9c9a0613bd","name":"Constants Node","func":"msg.topic = \"constants\";\n\nmsg.payload = {\n\n SETPOINT: 277.59,\n\n START_DIFF: 1.1,\n\n MIN_OFF_TIME: 300,\n\n MIN_RUN_TIME: 180,\n\n FAN_OFF: 0,\n FAN_SETPOINT: 0,\n FAN_PULLDOWN: 70,\n FAN_COOLING: 85\n\n};\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":340,"y":80,"wires":[["0b72854cc33bc6b1"]]},{"id":"8b6b09408a1e36aa","type":"global-config","env":[],"modules":{"@signalk/node-red-embedded":"2.21.0","node-red-contrib-shelly":"11.11.2"}}]
It was easy enough and worked well as it was. Then I had the idea to hand it over to ChatGPT and it has helped me build a much more robust system with safety checks, delayed start ups, clean initialization, and room for expansion (defrost cycles, door open events, etc.). Refrigeration V2.0 is up and running. If currently also drives a variable speed circulation fan (run by a shelly RGBW) with room for a defrost fan and cooling lift fan I plan to install soon. The circulation fan does wonders at keeping the whole box even, reducing load on the compressor, ice build up on the evaporator, etc. This uses 1 ruuvi tag and two Shellys - one on/off for the compressor and 1 dimmable for the fan(s). There is a simple "config" node for constant setting such as temperature set point, fan speeds, etc. in an easy to access place.
[{"id":"1bf00d9c9a0613bd","type":"tab","label":"Refrigerator V2","disabled":false,"info":"","env":[]},{"id":"49bac916bcf7bd87","type":"signalk-subscribe","z":"1bf00d9c9a0613bd","name":"Refrigerator Temperature","mode":"sendAll","flatten":true,"context":"vessels.self","path":"environment.inside.refrigerator.temperature","source":"","period":1000,"x":150,"y":180,"wires":[["85ca6a094da20e80"]]},{"id":"68036bea9ccc7429","type":"function","z":"1bf00d9c9a0613bd","name":"Pole Compressor Relay","func":"msg.payload = {\n method: \"Switch.GetStatus\",\n parameters: {\n id: 0\n }\n};\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":330,"y":260,"wires":[["6b136983817d7bdd"]]},{"id":"6b136983817d7bdd","type":"shelly-gen2","z":"1bf00d9c9a0613bd","hostname":"172.16.1.110","description":"Compressor","mode":"polling","verbose":false,"server":"","outputmode":"event","uploadretryinterval":5000,"pollinginterval":5000,"pollstatus":false,"getstatusoncommand":true,"devicetype":"Relay","devicetypemustmatchexactly":false,"captureblutooth":false,"outputs":1,"x":170,"y":320,"wires":[["2a355efa79f69d1e"]]},{"id":"2a355efa79f69d1e","type":"function","z":"1bf00d9c9a0613bd","name":"Pole Result","func":"msg.payload = Number(msg.payload.switch0.output);\nmsg.topic = \"compressor\";\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":370,"y":320,"wires":[["6e5eebe2e9fb6524"]]},{"id":"85ca6a094da20e80","type":"change","z":"1bf00d9c9a0613bd","name":"topic","rules":[{"t":"set","p":"topic","pt":"msg","to":"temp","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":430,"y":180,"wires":[["0b72854cc33bc6b1"]]},{"id":"0b72854cc33bc6b1","type":"function","z":"1bf00d9c9a0613bd","name":"State Cache","func":"let state = context.get(\"state\") || {};\n\nstate[msg.topic] = msg.payload;\nif (msg.topic === \"constants\") {\n state.constants = msg.payload;\n}\n\n// ----------------------------------------\n// NORMALIZE TYPES\n// ----------------------------------------\n\nif (state.temp !== undefined) {\n state.temp = Number(state.temp);\n}\n\nif (state.setpoint !== undefined) {\n state.setpoint = Number(state.setpoint);\n}\n\nif (state.compressor !== undefined) {\n state.compressor = Number(state.compressor);\n}\n\nif (state.compressorRunSeconds !== undefined) {\n state.compressorRunSeconds =\n Number(state.compressorRunSeconds);\n}\n\nif (state.compressorOffSeconds !== undefined) {\n state.compressorOffSeconds =\n Number(state.compressorOffSeconds);\n}\n\nif (state.systemEnabled !== undefined) {\n state.systemEnabled =\n Number(state.systemEnabled) === 1;\n}\n\ncontext.set(\"state\", state);\n\nmsg.payload = state;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":770,"y":260,"wires":[["9b381e1898b746b9"]]},{"id":"74d9cd071890e077","type":"inject","z":"1bf00d9c9a0613bd","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"60","crontab":"","once":true,"onceDelay":"5","topic":"","payload":"","payloadType":"date","x":110,"y":40,"wires":[["7a699e4f8437c853","2e53cfb5c781239e"]]},{"id":"7a699e4f8437c853","type":"change","z":"1bf00d9c9a0613bd","name":"setpoint","rules":[{"t":"set","p":"payload","pt":"msg","to":"277.59","tot":"num"},{"t":"set","p":"topic","pt":"msg","to":"setpoint","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":320,"y":40,"wires":[["0b72854cc33bc6b1","cf8f11130e2d4dfd"]]},{"id":"cf8f11130e2d4dfd","type":"change","z":"1bf00d9c9a0613bd","name":"to SK","rules":[{"t":"set","p":"topic","pt":"msg","to":"environment.inside.refrigerator.setting","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":530,"y":40,"wires":[["a0c7a1f4eccdd3fe"]]},{"id":"a0c7a1f4eccdd3fe","type":"signalk-send-pathvalue","z":"1bf00d9c9a0613bd","name":"","path":"","source":"","meta":"","x":810,"y":40,"wires":[]},{"id":"9b381e1898b746b9","type":"function","z":"1bf00d9c9a0613bd","name":"Timer","func":"let s = msg.payload;\n\n// Current compressor state\nlet compressor = Number(s.compressor || 0);\n\n// Previous state\nlet lastCompressor = context.get(\"lastCompressor\");\nif (lastCompressor === undefined) {\n lastCompressor = compressor;\n}\n\n// Timers\nlet compressorRunSeconds =\n context.get(\"compressorRunSeconds\") || 0;\n\nlet compressorOffSeconds =\n context.get(\"compressorOffSeconds\") || 0;\n\n// Poll interval\nconst POLL_INTERVAL = 5;\n\n// State machine\nif (compressor === 1) {\n\n compressorRunSeconds += POLL_INTERVAL;\n compressorOffSeconds = 0;\n\n} else {\n\n compressorOffSeconds += POLL_INTERVAL;\n compressorRunSeconds = 0;\n\n}\n\n// Save state\ncontext.set(\"compressorRunSeconds\", compressorRunSeconds);\ncontext.set(\"compressorOffSeconds\", compressorOffSeconds);\ncontext.set(\"lastCompressor\", compressor);\n\n// Add to payload\ns.compressorRunSeconds = compressorRunSeconds;\ns.compressorOffSeconds = compressorOffSeconds;\n\nmsg.payload = s;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":130,"y":380,"wires":[["228f6e46c43c46b3"]]},{"id":"c89f0a81a5ea1717","type":"function","z":"1bf00d9c9a0613bd","name":"Compressor Controller","func":"let s = msg.payload;\n\n// ----------------------------------------\n// SETTINGS\n// ----------------------------------------\n\nconst MIN_OFF_TIME =\n s.constants.MIN_OFF_TIME;\n\nconst MIN_RUN_TIME =\n s.constants.MIN_RUN_TIME;\n\nlet compressorCommand = null;\n\nif (!s.systemEnabled) {\n\n msg.payload = {\n compressor: 0\n };\n\n return msg;\n}\n\n// ----------------------------------------\n// START COMPRESSOR\n// ----------------------------------------\n\nif (\n !s.coolingActive &&\n s.coolingDemand &&\n s.compressorOffSeconds >= MIN_OFF_TIME\n) {\n compressorCommand = 1;\n}\n\n// ----------------------------------------\n// STOP COMPRESSOR\n// ----------------------------------------\n\nelse if (\n s.coolingActive &&\n s.atSetpoint &&\n s.compressorRunSeconds >= MIN_RUN_TIME\n) {\n compressorCommand = 0;\n}\n\n// ----------------------------------------\n// NO CHANGE\n// ----------------------------------------\n\nif (compressorCommand === null) {\n return null;\n}\n\n// ----------------------------------------\n// OUTPUT\n// ----------------------------------------\n\nmsg.payload = {\n compressor: compressorCommand\n};\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":160,"y":660,"wires":[["279c40bcb0457f54"]]},{"id":"279c40bcb0457f54","type":"function","z":"1bf00d9c9a0613bd","name":"Shelly Control","func":"if (msg.payload.compressor === 1) {\n\n msg.payload = {\n method: \"Switch.Set\",\n parameters: {\n id: 0,\n on: true\n }\n };\n\n return msg;\n}\n\nif (msg.payload.compressor === 0) {\n\n msg.payload = {\n method: \"Switch.Set\",\n parameters: {\n id: 0,\n on: false\n }\n };\n\n return msg;\n}\n\nreturn null;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":400,"y":660,"wires":[["aba6b38a4343b153"]]},{"id":"228f6e46c43c46b3","type":"function","z":"1bf00d9c9a0613bd","name":"Refrigeration State","func":"let s = msg.payload;\n\n// ----------------------------------------\n// TEMPERATURE STATE\n// ----------------------------------------\n\nconst START_DIFF =\n s.constants.START_DIFF;\n\ns.coolingDemand =\n s.temp >= (s.setpoint + START_DIFF);\n\ns.atSetpoint =\n s.temp <= s.setpoint;\n\n// ----------------------------------------\n// COMPRESSOR STATE\n// ----------------------------------------\n\ns.coolingActive =\n s.compressor === 1;\n\n// ----------------------------------------\n// FUTURE STATES\n// ----------------------------------------\n\n// placeholders for future expansion\n\ns.defrostActive = false;\n\ns.coolLiftActive = false;\n\n// ----------------------------------------\n// OUTPUT\n// ----------------------------------------\n\nmsg.payload = s;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":310,"y":380,"wires":[["3f62a5848505c5ea"]]},{"id":"3f62a5848505c5ea","type":"function","z":"1bf00d9c9a0613bd","name":"Mode Manager","func":"let s = msg.payload;\n\n// ----------------------------------------\n// SYSTEM OFF\n// ----------------------------------------\n\nif (!s.systemEnabled) {\n\n s.mode = \"OFF\";\n}\n\n// ----------------------------------------\n// COOLING\n// ----------------------------------------\n\nelse if (s.coolingActive) {\n\n s.mode = \"COOLING\";\n}\n\n// ----------------------------------------\n// PULLDOWN\n// ----------------------------------------\n\nelse if (s.coolingDemand) {\n\n s.mode = \"PULLDOWN\";\n}\n\n// ----------------------------------------\n// AT SETPOINT\n// ----------------------------------------\n\nelse {\n\n s.mode = \"AT_SETPOINT\";\n}\n\nmsg.payload = s;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":180,"y":440,"wires":[["c89f0a81a5ea1717","3e51e306ca707f44","1f818feaf71ec23f","c84ffb40f6bbafc4"]]},{"id":"89c2d075dec204bd","type":"signalk-send-pathvalue","z":"1bf00d9c9a0613bd","name":"Status","path":"environment.inside.refrigerator.mode","source":"","meta":"","x":590,"y":520,"wires":[]},{"id":"3e51e306ca707f44","type":"function","z":"1bf00d9c9a0613bd","name":"Status Publisher","func":"let s = msg.payload;\n\nlet status = \"\";\n\nswitch (s.mode) {\n\n case \"OFF\":\n status = \"Off\";\n break;\n\n case \"AT_SETPOINT\":\n status = \"At Setpoint\";\n break;\n\n case \"PULLDOWN\":\n status = \"Pulling Down\";\n break;\n\n case \"COOLING\":\n status = \"Cooling\";\n break;\n\n case \"DEFROST\":\n status = \"Defrost\";\n break;\n\n case \"COOL_LIFT\":\n status = \"Cool Lift\";\n break;\n\n default:\n status = \"Unknown\";\n}\n\nmsg.payload = status;\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":400,"y":500,"wires":[["89c2d075dec204bd"]]},{"id":"1f818feaf71ec23f","type":"function","z":"1bf00d9c9a0613bd","name":"Circulation Fan Controller","func":"let s = msg.payload;\n\n// ----------------------------------------\n// FAN SPEED SETTINGS\n//\n// Current blower is noisy below 70%.\n// Future PWM fan:\n//\n// OFF = 0\n// SETPOINT = 30\n// PULLDOWN = 70\n// COOLING = 85\n//\n// ----------------------------------------\n\nconst FAN_OFF =\n s.constants.FAN_OFF;\n\nconst FAN_SETPOINT =\n s.constants.FAN_SETPOINT;\n\nconst FAN_PULLDOWN =\n s.constants.FAN_PULLDOWN;\n\nconst FAN_COOLING =\n s.constants.FAN_COOLING;\n\n// ----------------------------------------\n// MODE LOGIC\n// ----------------------------------------\n\nlet fanSpeed = FAN_OFF;\n\nif (!s.systemEnabled) {\n fanSpeed = FAN_OFF;\n}\n\nelse if (s.mode === \"AT_SETPOINT\") {\n fanSpeed = FAN_SETPOINT;\n}\n\nelse if (s.mode === \"PULLDOWN\") {\n fanSpeed = FAN_PULLDOWN;\n}\n\nelse if (s.mode === \"COOLING\") {\n fanSpeed = FAN_COOLING;\n}\n\n// ----------------------------------------\n// OUTPUT\n// ----------------------------------------\n\nmsg.payload = fanSpeed;\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":170,"y":540,"wires":[["b8ee811fe694a2c3"]]},{"id":"b8ee811fe694a2c3","type":"function","z":"1bf00d9c9a0613bd","name":"Fan Output Mapper","func":"let speed = Number(msg.payload || 0);\n\nmsg.payload = {\n light: 0,\n brightness: speed,\n timer: 0,\n on: speed > 0\n};\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":410,"y":540,"wires":[["1002e82ffa540a09"]]},{"id":"5555bac5cf3bc343","type":"change","z":"1bf00d9c9a0613bd","name":"topic","rules":[{"t":"set","p":"topic","pt":"msg","to":"electrical.switches.fridgeSwitch.state","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":590,"y":140,"wires":[["dcd1a106a9382490"]]},{"id":"dcd1a106a9382490","type":"signalk-send-pathvalue","z":"1bf00d9c9a0613bd","name":"","path":"","source":"","meta":"","x":810,"y":140,"wires":[]},{"id":"c84ffb40f6bbafc4","type":"function","z":"1bf00d9c9a0613bd","name":"State Publisher","func":"let s = msg.payload;\n\n// Output 1\nmsg.payload = s.coolingDemand;\nmsg.topic = \"environment.inside.refrigerator.coolingDemand\";\n\n// Output 2\nlet msg2 = {\n topic: \"environment.inside.refrigerator.coolingActive\",\n payload: s.coolingActive\n};\n\n// Output 3\nlet msg3 = {\n topic: \"environment.inside.refrigerator.compressorRunSeconds\",\n payload: s.compressorRunSeconds\n};\n\n// Output 4\nlet msg4 = {\n topic: \"environment.inside.refrigerator.compressorOffSeconds\",\n payload: s.compressorOffSeconds\n};\n\nlet msg5 = {\n\n topic: \"environment.inside.refrigerator.atSetpoint\",\n\n payload: s.atSetpoint\n\n};\n\nreturn [msg, msg2, msg3, msg4, msg5];","outputs":5,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":560,"y":440,"wires":[["3ddcb2d94ed3b3d3"],["ac09af3f974d82ed"],["e884104019d5ad9b"],["806f69a1e74bacef"],["ec4964c669ff8193"]]},{"id":"3ddcb2d94ed3b3d3","type":"signalk-send-pathvalue","z":"1bf00d9c9a0613bd","name":"coolingActive","path":"environment.inside.refrigerator.coolingActive","source":"","meta":"","x":840,"y":340,"wires":[]},{"id":"ac09af3f974d82ed","type":"signalk-send-pathvalue","z":"1bf00d9c9a0613bd","name":"coolingDemand","path":"environment.inside.refrigerator.coolingDemand","source":"","meta":"","x":840,"y":400,"wires":[]},{"id":"e884104019d5ad9b","type":"signalk-send-pathvalue","z":"1bf00d9c9a0613bd","name":"compressorRunSeconds","path":"environment.inside.refrigerator.compressorRunSeconds","source":"","meta":"","x":810,"y":460,"wires":[]},{"id":"806f69a1e74bacef","type":"signalk-send-pathvalue","z":"1bf00d9c9a0613bd","name":"compressorOffSeconds","path":"environment.inside.refrigerator.compressorOffSeconds","source":"","meta":"","x":810,"y":520,"wires":[]},{"id":"6eafb2d03cf63f01","type":"signalk-send-pathvalue","z":"1bf00d9c9a0613bd","name":"","path":"","source":"","meta":"","x":810,"y":640,"wires":[]},{"id":"1002e82ffa540a09","type":"shelly-gen1","z":"1bf00d9c9a0613bd","hostname":"172.16.1.105","description":"FridgeFans","mode":"polling","verbose":false,"server":"","outputmode":"event","uploadretryinterval":5000,"pollinginterval":5000,"pollstatus":false,"getstatusoncommand":true,"devicetype":"RGBW","devicetypemustmatchexactly":false,"outputs":1,"x":390,"y":600,"wires":[["2bea9a0980918f11"]]},{"id":"2bea9a0980918f11","type":"function","z":"1bf00d9c9a0613bd","name":"Fan Speed","func":"msg.payload = msg.payload.lights[0].brightness;\nmsg.payload = Number(msg.payload);\nmsg.payload = msg.payload / 100;\nmsg.topic = 'electrical.switches.refrigeratorCircFan.speed';\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":570,"y":600,"wires":[["6eafb2d03cf63f01"]]},{"id":"aba6b38a4343b153","type":"shelly-gen2","z":"1bf00d9c9a0613bd","hostname":"172.16.1.110","description":"On/Off","mode":"polling","verbose":false,"server":"","outputmode":"event","uploadretryinterval":5000,"pollinginterval":5000,"pollstatus":false,"getstatusoncommand":true,"devicetype":"Relay","devicetypemustmatchexactly":false,"captureblutooth":false,"outputs":1,"x":590,"y":660,"wires":[["b2f551245ced9c3c"]]},{"id":"b2f551245ced9c3c","type":"switch","z":"1bf00d9c9a0613bd","name":"","property":"payload.switch0.output","propertyType":"msg","rules":[{"t":"false"},{"t":"true"}],"checkall":"true","repair":false,"outputs":2,"x":370,"y":740,"wires":[["b8bb04c4b6f12522"],["5a97b03bc3061b8a"]]},{"id":"b8bb04c4b6f12522","type":"change","z":"1bf00d9c9a0613bd","name":"Off","rules":[{"t":"set","p":"payload","pt":"msg","to":"Off","tot":"str"},{"t":"set","p":"topic","pt":"msg","to":"electrical.switches.refrigeratorCompressor.status","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":590,"y":720,"wires":[["33e4aa3179dd5dba"]]},{"id":"5a97b03bc3061b8a","type":"change","z":"1bf00d9c9a0613bd","name":"On","rules":[{"t":"set","p":"payload","pt":"msg","to":"Running","tot":"str"},{"t":"set","p":"topic","pt":"msg","to":"electrical.switches.refrigeratorCompressor.status","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":590,"y":760,"wires":[["33e4aa3179dd5dba"]]},{"id":"33e4aa3179dd5dba","type":"signalk-send-pathvalue","z":"1bf00d9c9a0613bd","name":"","path":"","source":"","meta":"","x":810,"y":740,"wires":[]},{"id":"d385a50b4b7c73aa","type":"signalk-put-handler","z":"1bf00d9c9a0613bd","name":"","path":"electrical.switches.fridgeSwitch.state","pending":false,"x":130,"y":140,"wires":[["1902dd6903e7ffe2"]]},{"id":"1902dd6903e7ffe2","type":"change","z":"1bf00d9c9a0613bd","name":"topic","rules":[{"t":"set","p":"topic","pt":"msg","to":"systemEnabled","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":430,"y":140,"wires":[["5555bac5cf3bc343","0b72854cc33bc6b1"]]},{"id":"6e5eebe2e9fb6524","type":"function","z":"1bf00d9c9a0613bd","name":"Startup Mgr","func":"let initialized = context.get(\"initialized\") || false;\n\n// only run once after deploy\n\nif (!initialized) {\n let compressor = Number(msg.payload);\n if (compressor === 1) {\n msg.compressorRunSeconds = 180;\n msg.compressorOffSeconds = 0;\n } else {\n msg.compressorRunSeconds = 0;\n msg.compressorOffSeconds = 300;\n }\n context.set(\"initialized\", true);\n}\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":550,"y":320,"wires":[["0b72854cc33bc6b1"]]},{"id":"3676a22c1ffe4a8c","type":"signalk-get","z":"1bf00d9c9a0613bd","name":"Get Swicth","path":"electrical.switches.fridgeSwitch.state","x":290,"y":220,"wires":[["ff210961e6a83fc0"],[]]},{"id":"4789d200a1c739ac","type":"inject","z":"1bf00d9c9a0613bd","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"5","crontab":"","once":true,"onceDelay":"5","topic":"","payload":"","payloadType":"date","x":110,"y":240,"wires":[["68036bea9ccc7429","3676a22c1ffe4a8c"]]},{"id":"ec4964c669ff8193","type":"signalk-send-pathvalue","z":"1bf00d9c9a0613bd","d":true,"name":"","path":"","source":"","meta":"","x":810,"y":580,"wires":[]},{"id":"ff210961e6a83fc0","type":"change","z":"1bf00d9c9a0613bd","name":"topic","rules":[{"t":"set","p":"topic","pt":"msg","to":"systemEnabled","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":550,"y":220,"wires":[["0b72854cc33bc6b1"]]},{"id":"2e53cfb5c781239e","type":"function","z":"1bf00d9c9a0613bd","name":"Constants Node","func":"msg.topic = \"constants\";\n\nmsg.payload = {\n\n SETPOINT: 277.59,\n\n START_DIFF: 1.1,\n\n MIN_OFF_TIME: 300,\n\n MIN_RUN_TIME: 180,\n\n FAN_OFF: 0,\n FAN_SETPOINT: 0,\n FAN_PULLDOWN: 70,\n FAN_COOLING: 85\n\n};\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":340,"y":80,"wires":[["0b72854cc33bc6b1"]]},{"id":"8b6b09408a1e36aa","type":"global-config","env":[],"modules":{"@signalk/node-red-embedded":"2.21.0","node-red-contrib-shelly":"11.11.2"}}]
_____________
Nautical 32
TBD
Saugatuck, MI
Nautical 32
TBD
Saugatuck, MI

