Irrigation sequence fail

Objective: using a Wittboy GW2000A I want to calculate the ET0 and activate the Waveshre POE ETH Relay to watering 8 different zone of my garden.
Using an ai assisted page (Calude) because of my lack in programming skill, I’ve came up to correctly calculate the ET0 and injected the value to calculate the timer foe each zone for the watering of my garden.
The problem is: the “sequence controller (debug 4)” is switching the address 0 (channel 1) but don’t cicle the others channels.

Sequence controller:

[{"id":"0d78a8819d1f2ce8","type":"function","z":"d0a0029743190dc0","name":"Sequence Controller (debug 4)","func":"// Zone configuration with adjustments for different plant types\nconst zones = [\n    { channel: 1, priority: 1, flowRate: 15, adjustmentFactor: 1.0, name: 'Lawn Front' },\n    { channel: 2, priority: 2, flowRate: 15, adjustmentFactor: 0.8, name: 'Flower Bed' },\n    { channel: 3, priority: 3, flowRate: 15, adjustmentFactor: 0.7, name: 'Vegetables' },\n    { channel: 4, priority: 4, flowRate: 15, adjustmentFactor: 1.2, name: 'New Plants' },\n    { channel: 5, priority: 5, flowRate: 15, adjustmentFactor: 1.0, name: 'Lawn Back' },\n    { channel: 6, priority: 6, flowRate: 15, adjustmentFactor: 0.6, name: 'Shrubs' },\n    { channel: 7, priority: 7, flowRate: 15, adjustmentFactor: 0.5, name: 'Trees' },\n    { channel: 8, priority: 8, flowRate: 15, adjustmentFactor: 1.0, name: 'Side Yard' }\n];\n\n// Function to create properly formatted Modbus message\nfunction createModbusMsg(value, address) {\n    return {\n        payload: {\n            value: value ? true : false,  // Convert to boolean\n            fc: 5,                        // Force Single Coil\n            unitid: 1,                    // Modbus unit ID\n            address: address,             // Coil address (0-based)\n            quantity: 1                   // Always 1 for single coil\n        }\n    };\n}\n\nlet currentZone = 0;\nlet isIrrigating = false;\nlet zoneEndTime = null;\n\n// Function to turn off a zone\nfunction turnOffZone(zoneIndex) {\n    const modbusMsg = createModbusMsg(false, zones[zoneIndex].channel - 1);\n    \n    // Add zone info for debugging\n    modbusMsg.zoneInfo = {\n        name: zones[zoneIndex].name,\n        channel: zones[zoneIndex].channel,\n        action: 'OFF'\n    };\n    \n    return modbusMsg;\n}\n\n// Function to turn on a zone\nfunction turnOnZone(zoneIndex, duration) {\n    const modbusMsg = createModbusMsg(true, zones[zoneIndex].channel - 1);\n    \n    // Add zone info for debugging\n    modbusMsg.zoneInfo = {\n        name: zones[zoneIndex].name,\n        channel: zones[zoneIndex].channel,\n        duration: duration,\n        action: 'ON'\n    };\n    \n    return modbusMsg;\n}\n\n// Main flow logic\nfunction processIrrigationCycle() {\n    const now = new Date();\n    \n    // If we're currently irrigating, check if the current zone needs to be turned off\n    if (isIrrigating && zoneEndTime && now >= new Date(zoneEndTime)) {\n        // Turn off current zone\n        node.send([turnOffZone(currentZone), null]);\n        \n        // Move to the next zone\n        currentZone = (currentZone + 1) % zones.length;\n        \n        // Reset irrigation if we've completed all zones\n        if (currentZone === 0) {\n            isIrrigating = false;\n            zoneEndTime = null;\n        } else {\n            // Calculate adjusted watering time for the new zone\n            const adjustedTime = Math.round(msg.payload.wateringTime * zones[currentZone].adjustmentFactor);\n            \n            // Set end time for the new zone\n            zoneEndTime = new Date(now.getTime() + (adjustedTime * 60 * 1000));\n            \n            // Turn on the new zone\n            node.send([turnOnZone(currentZone, adjustedTime), null]);\n        }\n        \n        // Update context\n        context.set('currentZone', currentZone);\n        context.set('isIrrigating', isIrrigating);\n        context.set('zoneEndTime', zoneEndTime);\n    }\n    \n    // If we're not currently irrigating, check if we have a water need\n    if (!isIrrigating && msg.payload && msg.payload.wateringTime > 0) {\n        isIrrigating = true;\n        \n        // Calculate adjusted watering time for the first zone\n        const adjustedTime = Math.round(msg.payload.wateringTime * zones[currentZone].adjustmentFactor);\n        \n        // Set end time for the first zone\n        zoneEndTime = new Date(now.getTime() + (adjustedTime * 60 * 1000));\n        \n        // Turn on the first zone\n        node.send([turnOnZone(currentZone, adjustedTime), null]);\n        \n        // Update context\n        context.set('isIrrigating', isIrrigating);\n        context.set('zoneEndTime', zoneEndTime);\n    }\n    \n    // Send status to second output\n    const status = {\n        payload: {\n            currentZone: currentZone,\n            zoneName: zones[currentZone].name,\n            isIrrigating: isIrrigating,\n            endTime: zoneEndTime,\n            timestamp: now.toISOString()\n        }\n    };\n    \n    // First output is Modbus message (or null), second output is status\n    node.send([null, status]);\n}\n\n// Call the main flow logic\nprocessIrrigationCycle();","outputs":1,"timeout":"","noerr":0,"initialize":"// Initialize context variables\ncontext.set('currentZone', 0);\ncontext.set('isIrrigating', false);\ncontext.set('zoneEndTime', null);","finalize":"// Clean up on deploy\nif (context.get('isIrrigating')) {\n    // Turn off current zone\n    const currentZone = context.get('currentZone') || 0;\n    node.send({\n        payload: {\n            'value': 0,\n            'fc': 5,\n            'address': currentZone\n        }\n    });\n}","libs":[],"x":1370,"y":920,"wires":[["c541f94e9d858c5d","09f6e23668a9d7c3","70d9add1645d8bee"]]}]

JSON is invalid. After export and copy to clipboard select the code box </> above the editor and paste your code where indicated.

1 Like

There should be a second output on the function node for the status message. Turn the output up to 2 on the func nodes setup page.

image

How are you triggering this node? This needs to be triggered every minute when it is running. For example an inject node set to repeat every minute.

Ok, i’ve turned a second output in the func node and moved the status message on out2. And added an inject node set to repeat every 10s (i’m testing it on my bench before connecting to the irrigation valves).

I’ll check if the channel will change and keep you updated.
The “End Time:” looks like is null

The message from the watering time func “debug 20”:

{"waterNeed":"0.98","wateringTime":6,"weeklyRainTotal":"0.00","et0":0.98,"dailyRain":0,"timestamp":"2024-11-09T13:20:51.931Z"}

Message from “Zone Status”:

{"currentZone":0,"zoneName":"Lawn Front","isIrrigating":false,"endTime":null,"timestamp":"2024-11-09T13:23:15.656Z"}

Checking the channels:

[true,false,false,false,false,false,false,false]

So channel 0 is on, but the irrigation should be off?
Anyway is not changing channel.

Looking over the code it depends on data that is stored as context. It saves the context but does not recall it. Even if I change the code block to pull in the context it still doesn’t work right. It also needs a variable sent in for watering time.

let currentZone = context.get("currentZone") || 0;
let isIrrigating = context.get("isIrrigating") || false;
let zoneEndTime = context.get("zoneEndTime") || null;

image

Ok, maybe if I post the entire flow all the variable senti in. I need to split the code in two. Part 1 of 2.

[{"id":"dde1f6803602fab5","type":"function","z":"d0a0029743190dc0","name":"Validate Inputs","func":"// Define constants\nconst LATITUDE = 45.7731224;\nconst ELEVATION = 13;\n\n// Get inputs from msg or environment\n// let inputData;\n// try {\n//     inputData = (typeof msg.payload === 'string') ? JSON.parse(msg.payload) : msg.payload;\n// } catch (e) {\n//     return { payload: { error: 'Invalid JSON input' } };\n// }\n\n// const temp = inputData.temperature;\nconst temp = global.get(\"temperature\");\n// const solar = inputData.solarRadiation;\nconst solar = global.get(\"solarRadiation\");\n// const rh = inputData.relativeHumidity;\nconst rh = global.get(\"relativeHumidity\");\n// const wind = inputData.windSpeed;\nconst wind = global.get(\"windSpeed\");\n\n// Input validation\nif (temp === undefined || typeof temp !== 'number' || temp < -50 || temp > 60) {\n    return { payload: { error: 'Invalid temperature. Must be between -50°C and 60°C' } };\n}\n\nif (solar === undefined || typeof solar !== 'number' || solar < 0 || solar > 1500) {\n    return { payload: { error: 'Invalid solar radiation. Must be between 0 and 1500 W/m²' } };\n}\n\nif (rh === undefined || typeof rh !== 'number' || rh < 0 || rh > 100) {\n    return { payload: { error: 'Invalid relative humidity. Must be between 0% and 100%' } };\n}\n\nif (wind === undefined || typeof wind !== 'number' || wind < 0 || wind > 100) {\n    return { payload: { error: 'Invalid wind speed. Must be between 0 and 100 m/s' } };\n}\n\n// If all inputs are valid, pass them along with constants\nmsg.payload = {\n    temperature: temp,\n    solarRadiation: solar,\n    relativeHumidity: rh,\n    windSpeed: wind,\n    latitude: LATITUDE,\n    elevation: ELEVATION\n};\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":480,"y":520,"wires":[["c62a0881ebd5b74d"]]},{"id":"03b4607f42b375ef","type":"function","z":"d0a0029743190dc0","name":"Format Output","func":"try {\n    if (msg.payload.error) {\n        node.status({fill:\"red\",shape:\"ring\",text:msg.payload.error});\n        // Add error logging here if needed\n    } else {\n        node.status({fill:\"green\",shape:\"dot\",text:\"ET: \" + msg.payload.ET + \" mm/day\"});\n    }\n    return msg;\n} catch (error) {\n    node.status({fill:\"red\",shape:\"ring\",text:\"Error in output formatting\"});\n    return { payload: { error: 'Output formatting error', details: error.message } };\n}","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1040,"y":520,"wires":[["adf9e1769d5e05b2","7e2e3f234f0a0dd7","f5697da887a54c75"]]},{"id":"adf9e1769d5e05b2","type":"debug","z":"d0a0029743190dc0","name":"Debug Output","active":false,"tosidebar":true,"console":true,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1720,"y":520,"wires":[]},{"id":"c62a0881ebd5b74d","type":"function","z":"d0a0029743190dc0","name":"Calculate ET","func":"try {\n    // Function to calculate day of year\n    function getDayOfYear(date) {\n        const start = new Date(date.getFullYear(), 0, 0);\n        const diff = (date.getTime() - start.getTime());\n        const oneDay = 1000 * 60 * 60 * 24;\n        return Math.floor(diff / oneDay);\n    }\n\n    // Constants\n    const SOLAR_CONSTANT = 0.0820; // MJ m-2 min-1\n    const STEFAN_BOLTZMANN = 4.903e-9; // MJ K-4 m-2 day-1\n    const ATMOSPHERIC_PRESSURE = 101.3 * Math.pow((293 - 0.0065 * msg.payload.elevation) / 293, 5.26);\n    const PSYCHROMETRIC_CONSTANT = 0.000665 * ATMOSPHERIC_PRESSURE;\n\n    // Get current day of year\n    const currentDate = new Date();\n    const j = getDayOfYear(currentDate);\n\n    // Convert inputs to required units\n    const tempC = msg.payload.temperature;\n    const tempK = tempC + 273.16;\n    const solar = msg.payload.solarRadiation * 0.0864; // Convert W/m² to MJ/m²/day\n    const rh = msg.payload.relativeHumidity / 100;\n    const wind = msg.payload.windSpeed;\n\n    // Calculate saturation vapor pressure\n    const es = 0.6108 * Math.exp(17.27 * tempC / (tempC + 237.3));\n\n    // Calculate actual vapor pressure\n    const ea = es * rh;\n\n    // Calculate slope of saturation vapor pressure curve\n    const delta = 4098 * es / Math.pow(tempC + 237.3, 2);\n\n    // Calculate net radiation\n    const dr = 1 + 0.033 * Math.cos(2 * Math.PI / 365 * j);\n    const phi = msg.payload.latitude * Math.PI / 180;\n    const delta_solar = 0.409 * Math.sin(2 * Math.PI / 365 * j - 1.39);\n    \n    // Check for valid solar angle calculation\n    const tanPhi = Math.tan(phi);\n    const tanDelta = Math.tan(delta_solar);\n    const angleProduct = -tanPhi * tanDelta;\n    \n    if (angleProduct >= 1 || angleProduct <= -1) {\n        throw new Error('Invalid solar angle calculation: latitude or day of year out of valid range');\n    }\n    \n    const ws = Math.acos(angleProduct);\n\n    const Ra = 24 * 60 / Math.PI * SOLAR_CONSTANT * dr * \n        (ws * Math.sin(phi) * Math.sin(delta_solar) + \n         Math.cos(phi) * Math.cos(delta_solar) * Math.sin(ws));\n\n    const Rso = (0.75 + 2e-5 * msg.payload.elevation) * Ra;\n    const Rns = (1 - 0.23) * solar;\n    \n    // Add check for division by zero\n    if (Rso === 0) {\n        throw new Error('Solar radiation calculation error: Rso is zero');\n    }\n    \n    const Rnl = STEFAN_BOLTZMANN * Math.pow(tempK, 4) * \n        (0.34 - 0.14 * Math.sqrt(ea)) * (1.35 * solar / Rso - 0.35);\n    const Rn = Rns - Rnl;\n\n    // Calculate soil heat flux (assumed negligible for daily calculation)\n    const G = 0;\n\n    // Calculate reference ET (FAO Penman-Monteith)\n    const numerator = 0.408 * delta * (Rn - G) + \n        PSYCHROMETRIC_CONSTANT * 900 / (tempK) * wind * (es - ea);\n    const denominator = delta + PSYCHROMETRIC_CONSTANT * (1 + 0.34 * wind);\n    \n    if (denominator === 0) {\n        throw new Error('Division by zero in ET calculation');\n    }\n    \n    const ET = numerator / denominator;\n\n    // Error check the result\n    // if (isNaN(ET) || ET < 0 || ET > 15) {\n    //     return { payload: {\n    //         error: 'Calculated ET is outside reasonable bounds',\n    //         ET: ET,\n    //         details: 'ET should be between 0 and 15 mm/day'\n    //     }};\n    // }\n\n    msg.payload = {\n        ET: parseFloat(ET.toFixed(2)),\n        timestamp: currentDate.toISOString(),\n        dayOfYear: j,\n        inputs: {\n            temperature: tempC,\n            solarRadiation: msg.payload.solarRadiation,\n            relativeHumidity: rh * 100,\n            windSpeed: wind\n        },\n        intermediateValues: {\n            saturationVaporPressure: parseFloat(es.toFixed(3)),\n            actualVaporPressure: parseFloat(ea.toFixed(3)),\n            netRadiation: parseFloat(Rn.toFixed(2)),\n            solarRadiationConverted: parseFloat(solar.toFixed(2)),\n            dayAngle: parseFloat(dr.toFixed(4)),\n            sunsetHourAngle: parseFloat(ws.toFixed(4)),\n            extraterrestrialRadiation: parseFloat(Ra.toFixed(2))\n        }\n    };\n\n    return msg;\n} catch (error) {\n    return { payload: { \n        error: 'Calculation error', \n        details: error.message,\n        timestamp: new Date().toISOString()\n    }};\n}","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":710,"y":520,"wires":[["03b4607f42b375ef"]]},{"id":"f58041629c8aa183","type":"api-get-history","z":"d0a0029743190dc0","name":"Temp °C History 24 h","server":"6a3c3921969e85fe","version":1,"startDate":"","endDate":"","entityId":"sensor.gw2000a_outdoor_temperature","entityIdType":"equals","useRelativeTime":true,"relativeTime":"24 h","flatten":true,"outputType":"array","outputLocationType":"msg","outputLocation":"payload","x":360,"y":120,"wires":[["b880048a7a4525f4"]]},{"id":"aa12511a165a7c45","type":"inject","z":"d0a0029743190dc0","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":"5","topic":"","payload":"","payloadType":"date","x":100,"y":40,"wires":[["1e9701a9f00475cf"]]},{"id":"f631997554862ae3","type":"function","z":"d0a0029743190dc0","name":"Calculate Mean","func":"// Get the array of numbers from input\nlet numbers = msg.payload;\n\nif (!Array.isArray(numbers)) {\n    node.error(\"Input must be an array of numbers\");\n    return null;\n}\n\n// Calculate mean\nlet sum = numbers.reduce((a, b) => a + b, 0);\nlet mean = sum / numbers.length;\n\n// Create output message\n// msg.payload = {\n//     original: numbers,\n//     mean: mean,\n//     count: numbers.length\n// };\n\nmsg.payload = mean\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1040,"y":120,"wires":[["d0ffbfe6b4693a1e"]]},{"id":"3594d0dc7f8541bd","type":"api-get-history","z":"d0a0029743190dc0","name":"Solar Rad W/m2/day History 24 h","server":"6a3c3921969e85fe","version":1,"startDate":"","endDate":"","entityId":"sensor.gw2000a_solar_radiation","entityIdType":"equals","useRelativeTime":true,"relativeTime":"24 h","flatten":true,"outputType":"array","outputLocationType":"msg","outputLocation":"payload","x":400,"y":220,"wires":[["41f32c90b3b3802a"]]},{"id":"31c08c4bf56ed98d","type":"function","z":"d0a0029743190dc0","name":"Calculate Mean","func":"// Get the array of numbers from input\nlet numbers = msg.payload;\n\nif (!Array.isArray(numbers)) {\n    node.error(\"Input must be an array of numbers\");\n    return null;\n}\n\n// Calculate mean\nlet sum = numbers.reduce((a, b) => a + b, 0);\nlet mean = sum / numbers.length;\n\n// Create output message\n// msg.payload = {\n//     original: numbers,\n//     mean: mean,\n//     count: numbers.length\n// };\n\nmsg.payload = mean\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1040,"y":220,"wires":[["c8335795bd759de7"]]},{"id":"ea6ff13749abb47a","type":"api-get-history","z":"d0a0029743190dc0","name":"Wind Speed m/s History 24 h","server":"6a3c3921969e85fe","version":1,"startDate":"","endDate":"","entityId":"sensor.gw2000a_wind_speed","entityIdType":"equals","useRelativeTime":true,"relativeTime":"24 h","flatten":true,"outputType":"array","outputLocationType":"msg","outputLocation":"payload","x":380,"y":320,"wires":[["c71da442b2c55e26"]]},{"id":"b6a168437f603ee7","type":"function","z":"d0a0029743190dc0","name":"Calculate Mean","func":"// Get the array of numbers from input\nlet numbers = msg.payload;\n\nif (!Array.isArray(numbers)) {\n    node.error(\"Input must be an array of numbers\");\n    return null;\n}\n\n// Calculate mean\nlet sum = numbers.reduce((a, b) => a + b, 0);\nlet mean = sum / numbers.length;\n\n// Create output message\n// msg.payload = {\n//     original: numbers,\n//     mean: mean,\n//     count: numbers.length\n// };\n\nmsg.payload = mean\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1040,"y":320,"wires":[["dd4574a6e57ecf15"]]},{"id":"6cd2bb86498ad0ec","type":"api-get-history","z":"d0a0029743190dc0","name":"Humidity % History 24 h","server":"6a3c3921969e85fe","version":1,"startDate":"","endDate":"","entityId":"sensor.gw2000a_humidity","entityIdType":"equals","useRelativeTime":true,"relativeTime":"24 h","flatten":true,"outputType":"array","outputLocationType":"msg","outputLocation":"payload","x":370,"y":420,"wires":[["ed0aa6d540b1b931"]]},{"id":"6494b8f4bc39874c","type":"function","z":"d0a0029743190dc0","name":"Calculate Mean","func":"// Get the array of numbers from input\nlet numbers = msg.payload;\n\nif (!Array.isArray(numbers)) {\n    node.error(\"Input must be an array of numbers\");\n    return null;\n}\n\n// Calculate mean\nlet sum = numbers.reduce((a, b) => a + b, 0);\nlet mean = sum / numbers.length;\n\n// Create output message\n// msg.payload = {\n//     original: numbers,\n//     mean: mean,\n//     count: numbers.length\n// };\n\nmsg.payload = mean\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1040,"y":420,"wires":[["2904a2fa1063f7b7"]]},{"id":"d0ffbfe6b4693a1e","type":"function","z":"d0a0029743190dc0","name":"Global Set","func":"global.set(\"temperature\", msg.payload);\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1290,"y":120,"wires":[["865de541331eb0aa","2145d0212b9e314c"]]},{"id":"c8335795bd759de7","type":"function","z":"d0a0029743190dc0","name":"Global Set","func":"global.set(\"solarRadiation\", msg.payload);\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1290,"y":220,"wires":[["19036dcf83b46a54","a1047b97b3798b3b"]]},{"id":"2904a2fa1063f7b7","type":"function","z":"d0a0029743190dc0","name":"Global Set","func":"global.set(\"relativeHumidity\", msg.payload);\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1290,"y":420,"wires":[["fe0df550dde56d51","e42ef451a23edc41"]]},{"id":"dd4574a6e57ecf15","type":"function","z":"d0a0029743190dc0","name":"Global Set","func":"global.set(\"windSpeed\", msg.payload);\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1290,"y":320,"wires":[["d6bbf3c8908e71df","2881dc60fa8bd2f2"]]},{"id":"7e2e3f234f0a0dd7","type":"split","z":"d0a0029743190dc0","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","property":"payload","x":1390,"y":580,"wires":[["17dd4f94a57fd640"]]},{"id":"17dd4f94a57fd640","type":"debug","z":"d0a0029743190dc0","name":"debug 1","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1700,"y":580,"wires":[]},{"id":"b880048a7a4525f4","type":"function","z":"d0a0029743190dc0","name":"Extract Numbers","func":"if (Array.isArray(msg.payload)) {\n    // Handle different history formats\n    try {\n        // If array contains objects with 'state' property\n        if (msg.payload[0].state !== undefined) {\n            msg.payload = msg.payload.map(item => {\n                // Convert string numbers to actual numbers\n                return Number(item.state);\n            });\n        }\n        // If array already contains numbers or number strings\n        else if (typeof msg.payload[0] === 'number' || !isNaN(msg.payload[0])) {\n            msg.payload = msg.payload.map(Number);\n        }\n        else {\n            node.error('Unsupported array format');\n            return null;\n        }\n        \n        // Filter out any NaN values\n        msg.payload = msg.payload.filter(num => !isNaN(num));\n        \n        return msg;\n    } catch (error) {\n        node.error('Error processing array: ' + error.message);\n        return null;\n    }\n} else {\n    node.error('Input must be an array');\n    return null;\n}","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":730,"y":120,"wires":[["f631997554862ae3"]]},{"id":"41f32c90b3b3802a","type":"function","z":"d0a0029743190dc0","name":"Extract Numbers","func":"if (Array.isArray(msg.payload)) {\n    // Handle different history formats\n    try {\n        // If array contains objects with 'state' property\n        if (msg.payload[0].state !== undefined) {\n            msg.payload = msg.payload.map(item => {\n                // Convert string numbers to actual numbers\n                return Number(item.state);\n            });\n        }\n        // If array already contains numbers or number strings\n        else if (typeof msg.payload[0] === 'number' || !isNaN(msg.payload[0])) {\n            msg.payload = msg.payload.map(Number);\n        }\n        else {\n            node.error('Unsupported array format');\n            return null;\n        }\n        \n        // Filter out any NaN values\n        msg.payload = msg.payload.filter(num => !isNaN(num));\n        \n        return msg;\n    } catch (error) {\n        node.error('Error processing array: ' + error.message);\n        return null;\n    }\n} else {\n    node.error('Input must be an array');\n    return null;\n}","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":730,"y":220,"wires":[["31c08c4bf56ed98d"]]},{"id":"c71da442b2c55e26","type":"function","z":"d0a0029743190dc0","name":"Extract Numbers","func":"if (Array.isArray(msg.payload)) {\n    // Handle different history formats\n    try {\n        // If array contains objects with 'state' property\n        if (msg.payload[0].state !== undefined) {\n            msg.payload = msg.payload.map(item => {\n                // Convert string numbers to actual numbers\n                return Number(item.state);\n            });\n        }\n        // If array already contains numbers or number strings\n        else if (typeof msg.payload[0] === 'number' || !isNaN(msg.payload[0])) {\n            msg.payload = msg.payload.map(Number);\n        }\n        else {\n            node.error('Unsupported array format');\n            return null;\n        }\n        \n        // Filter out any NaN values\n        msg.payload = msg.payload.filter(num => !isNaN(num));\n        \n        return msg;\n    } catch (error) {\n        node.error('Error processing array: ' + error.message);\n        return null;\n    }\n} else {\n    node.error('Input must be an array');\n    return null;\n}","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":730,"y":320,"wires":[["b6a168437f603ee7"]]},{"id":"ed0aa6d540b1b931","type":"function","z":"d0a0029743190dc0","name":"Extract Numbers","func":"if (Array.isArray(msg.payload)) {\n    // Handle different history formats\n    try {\n        // If array contains objects with 'state' property\n        if (msg.payload[0].state !== undefined) {\n            msg.payload = msg.payload.map(item => {\n                // Convert string numbers to actual numbers\n                return Number(item.state);\n            });\n        }\n        // If array already contains numbers or number strings\n        else if (typeof msg.payload[0] === 'number' || !isNaN(msg.payload[0])) {\n            msg.payload = msg.payload.map(Number);\n        }\n        else {\n            node.error('Unsupported array format');\n            return null;\n        }\n        \n        // Filter out any NaN values\n        msg.payload = msg.payload.filter(num => !isNaN(num));\n        \n        return msg;\n    } catch (error) {\n        node.error('Error processing array: ' + error.message);\n        return null;\n    }\n} else {\n    node.error('Input must be an array');\n    return null;\n}","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":730,"y":420,"wires":[["6494b8f4bc39874c"]]},{"id":"865de541331eb0aa","type":"link out","z":"d0a0029743190dc0","name":"link out 5","mode":"link","links":["d70af9b223acc85b"],"x":1445,"y":80,"wires":[]},{"id":"19036dcf83b46a54","type":"link out","z":"d0a0029743190dc0","name":"link out 6","mode":"link","links":["d70af9b223acc85b"],"x":1445,"y":180,"wires":[]},{"id":"d6bbf3c8908e71df","type":"link out","z":"d0a0029743190dc0","name":"link out 7","mode":"link","links":["d70af9b223acc85b"],"x":1445,"y":280,"wires":[]},{"id":"fe0df550dde56d51","type":"link out","z":"d0a0029743190dc0","name":"link out 8","mode":"link","links":["d70af9b223acc85b"],"x":1445,"y":380,"wires":[]},{"id":"d70af9b223acc85b","type":"link in","z":"d0a0029743190dc0","name":"link in 45","links":["19036dcf83b46a54","865de541331eb0aa","d6bbf3c8908e71df","fe0df550dde56d51"],"x":275,"y":520,"wires":[["dde1f6803602fab5"]]},{"id":"745a0be9a8e59aa1","type":"api-get-history","z":"d0a0029743190dc0","name":"Daily rain rate","server":"6a3c3921969e85fe","version":1,"startDate":"","endDate":"","entityId":"sensor.gw2000a_daily_rain_rate_piezo","entityIdType":"equals","useRelativeTime":true,"relativeTime":"24 h","flatten":true,"outputType":"split","outputLocationType":"msg","outputLocation":"payload","x":480,"y":640,"wires":[["55261693529b5b37"]]},{"id":"55261693529b5b37","type":"function","z":"d0a0029743190dc0","name":"parseFloat","func":"msg.payload = parseFloat(msg.payload.state);\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":730,"y":640,"wires":[["5d7c59e18a6e9488"]]},{"id":"5d7c59e18a6e9488","type":"function","z":"d0a0029743190dc0","name":"Daily Rain Rate","func":"global.set(\"dailyRain\", msg.payload);\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1300,"y":640,"wires":[[]]},{"id":"3a020d9760bd6e7a","type":"link in","z":"d0a0029743190dc0","name":"link in 47","links":["f5697da887a54c75"],"x":785,"y":740,"wires":[["18dd151e0cee26d0"]]},{"id":"18dd151e0cee26d0","type":"function","z":"d0a0029743190dc0","name":"Global Set","func":"global.set(\"et0\", msg.payload.ET);\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1290,"y":740,"wires":[[]]},{"id":"f5697da887a54c75","type":"link out","z":"d0a0029743190dc0","name":"link out 9","mode":"link","links":["3a020d9760bd6e7a","a0d299a7e9bf6792","6133e489ccb75478"],"x":1245,"y":480,"wires":[]},{"id":"1e9701a9f00475cf","type":"link out","z":"d0a0029743190dc0","name":"link out 10","mode":"link","links":["ddcdd6ab4a46516e","24726a812d11bfd7","badd12634fe37357","a1e165bfe3c8a6c7","0bb14c4f7aef5599","28326775a79aec49"],"x":265,"y":40,"wires":[]},{"id":"ddcdd6ab4a46516e","type":"link in","z":"d0a0029743190dc0","name":"link in 49","links":["1e9701a9f00475cf"],"x":165,"y":120,"wires":[["f58041629c8aa183"]]},{"id":"24726a812d11bfd7","type":"link in","z":"d0a0029743190dc0","name":"link in 50","links":["1e9701a9f00475cf"],"x":165,"y":220,"wires":[["3594d0dc7f8541bd"]]},{"id":"badd12634fe37357","type":"link in","z":"d0a0029743190dc0","name":"link in 51","links":["1e9701a9f00475cf"],"x":165,"y":320,"wires":[["ea6ff13749abb47a"]]},{"id":"a1e165bfe3c8a6c7","type":"link in","z":"d0a0029743190dc0","name":"link in 52","links":["1e9701a9f00475cf"],"x":165,"y":420,"wires":[["6cd2bb86498ad0ec"]]},{"id":"0bb14c4f7aef5599","type":"link in","z":"d0a0029743190dc0","name":"link in 53","links":["1e9701a9f00475cf"],"x":165,"y":640,"wires":[["745a0be9a8e59aa1"]]},{"id":"2145d0212b9e314c","type":"ui_text","z":"d0a0029743190dc0","group":"43e793fcea03b5bf","order":0,"width":0,"height":0,"name":"","label":"Temperature Mean: ","format":"{{msg.payload}}","layout":"row-spread","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1550,"y":140,"wires":[]},{"id":"a1047b97b3798b3b","type":"ui_text","z":"d0a0029743190dc0","group":"43e793fcea03b5bf","order":1,"width":0,"height":0,"name":"","label":"Solar Radiation Mean: ","format":"{{msg.payload}}","layout":"row-spread","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1560,"y":240,"wires":[]},{"id":"2881dc60fa8bd2f2","type":"ui_text","z":"d0a0029743190dc0","group":"43e793fcea03b5bf","order":2,"width":0,"height":0,"name":"","label":"Wind Speed Mean: ","format":"{{msg.payload}}","layout":"row-spread","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1550,"y":340,"wires":[]},{"id":"e42ef451a23edc41","type":"ui_text","z":"d0a0029743190dc0","group":"43e793fcea03b5bf","order":3,"width":0,"height":0,"name":"","label":"Humidity Mean: ","format":"{{msg.payload}}","layout":"row-spread","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1540,"y":440,"wires":[]},{"id":"5d2d70ed68b02343","type":"api-get-history","z":"d0a0029743190dc0","name":"Weekly rain rate","server":"6a3c3921969e85fe","version":1,"startDate":"","endDate":"","entityId":"sensor.gw2000a_weekly_rain_rate_piezo","entityIdType":"equals","useRelativeTime":true,"relativeTime":"24 h","flatten":true,"outputType":"split","outputLocationType":"msg","outputLocation":"payload","x":480,"y":700,"wires":[["4e37f76edc9b53d8"]]},{"id":"4e37f76edc9b53d8","type":"function","z":"d0a0029743190dc0","name":"parseFloat","func":"msg.payload = parseFloat(msg.payload.state);\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":730,"y":700,"wires":[["864a0523af67e901"]]},{"id":"864a0523af67e901","type":"function","z":"d0a0029743190dc0","name":"Wekly Rain Rate","func":"global.set(\"weeklyRain\", msg.payload);\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1310,"y":700,"wires":[[]]},{"id":"28326775a79aec49","type":"link in","z":"d0a0029743190dc0","name":"link in 55","links":["1e9701a9f00475cf"],"x":165,"y":700,"wires":[["5d2d70ed68b02343"]]},{"id":"6a3c3921969e85fe","type":"server","name":"Home Assistant","version":5,"addon":true,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":true,"cacheJson":true,"heartbeat":false,"heartbeatInterval":"30","areaSelector":"friendlyName","deviceSelector":"friendlyName","entitySelector":"friendlyName","statusSeparator":": ","statusYear":"hidden","statusMonth":"short","statusDay":"numeric","statusHourCycle":"default","statusTimeFormat":"h:m","enableGlobalContextStore":false},{"id":"43e793fcea03b5bf","type":"ui_group","name":"Default","tab":"c3514d7e422c425a","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"c3514d7e422c425a","type":"ui_tab","name":"Home","icon":"dashboard","disabled":false,"hidden":false}]

Parte 2 of 2

[{"id":"f9548c99b4430276","type":"function","z":"d0a0029743190dc0","name":"Calculate Water Need","func":"// Get weekly rain from context\n// let weeklyRain = context.get('weeklyRain') || 0;\nlet weeklyRain = global.get('weeklyRain') || 0;\n\n// Get input values with defaults\n// const et0 = msg.payload.et0 || 5;  // Default daily ET0 in mm\nconst et0 = global.get(\"et0\") || 5;  // Default daily ET0 in mm\n// const dailyRain = msg.payload.dailyRain || 0;  // Default daily rain in mm\nconst dailyRain = global.get(\"dailyRain\") || 0;  // Default daily rain in mm\nconst sprinklerRate = msg.payload.sprinklerRate || 10;  // Default sprinkler flow rate in mm/hour\n\n// Update weekly rain accumulation\nweeklyRain = weeklyRain + dailyRain;\ncontext.set('weeklyRain', weeklyRain);\n\n// Calculate water need\nlet waterNeed = et0 - dailyRain;\nif (waterNeed < 0) waterNeed = 0;\n\n// Calculate base watering time in minutes\nconst baseWateringTime = Math.round((waterNeed / sprinklerRate) * 60);\n\n// Apply minimum and maximum limits\nconst minWateringTime = 5; // 5 minutes minimum\nconst maxWateringTime = 30; // 30 minutes maximum\nlet wateringTime = Math.min(Math.max(baseWateringTime, minWateringTime), maxWateringTime);\n\n// Skip if weekly rain exceeds threshold\nconst weeklyRainThreshold = 25; // mm\nif (weeklyRain > weeklyRainThreshold) {\n    wateringTime = 0;\n}\n\n// Prepare output message\nmsg.payload = {\n    waterNeed: waterNeed.toFixed(2),\n    wateringTime: wateringTime,\n    weeklyRainTotal: weeklyRain.toFixed(2),\n    et0: et0,\n    dailyRain: dailyRain,\n    timestamp: new Date().toISOString()\n};\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1060,"y":860,"wires":[["0d78a8819d1f2ce8","37d2ade2d9c5c4b2"]]},{"id":"c541f94e9d858c5d","type":"modbus-flex-write","z":"d0a0029743190dc0","name":"Relay Control","showStatusActivities":true,"showErrors":true,"showWarnings":true,"server":"relay-server","emptyMsgOnFail":false,"keepMsgProperties":false,"delayOnStart":false,"startDelayTime":"","x":1720,"y":860,"wires":[[],["a72e058e18383329"]]},{"id":"09f6e23668a9d7c3","type":"debug","z":"d0a0029743190dc0","name":"Zone Status","active":false,"tosidebar":true,"console":true,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1720,"y":940,"wires":[]},{"id":"778c5da83e4f3bb1","type":"inject","z":"d0a0029743190dc0","name":"Sequence Check","props":[{"p":"payload"}],"repeat":"60","crontab":"","once":true,"onceDelay":"1","topic":"","payload":"","payloadType":"date","x":1050,"y":920,"wires":[["0d78a8819d1f2ce8","15f59032e962bf06"]]},{"id":"9724eb3843657c92","type":"delay","z":"d0a0029743190dc0","name":"","pauseType":"delay","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":880,"y":860,"wires":[["f9548c99b4430276"]]},{"id":"0d78a8819d1f2ce8","type":"function","z":"d0a0029743190dc0","name":"Sequence Controller (debug 4)","func":"// Zone configuration with adjustments for different plant types\nconst zones = [\n    { channel: 1, priority: 1, flowRate: 15, adjustmentFactor: 1.0, name: 'Lawn Front' },\n    { channel: 2, priority: 2, flowRate: 15, adjustmentFactor: 0.8, name: 'Flower Bed' },\n    { channel: 3, priority: 3, flowRate: 15, adjustmentFactor: 0.7, name: 'Vegetables' },\n    { channel: 4, priority: 4, flowRate: 15, adjustmentFactor: 1.2, name: 'New Plants' },\n    { channel: 5, priority: 5, flowRate: 15, adjustmentFactor: 1.0, name: 'Lawn Back' },\n    { channel: 6, priority: 6, flowRate: 15, adjustmentFactor: 0.6, name: 'Shrubs' },\n    { channel: 7, priority: 7, flowRate: 15, adjustmentFactor: 0.5, name: 'Trees' },\n    { channel: 8, priority: 8, flowRate: 15, adjustmentFactor: 1.0, name: 'Side Yard' }\n];\n\n// Function to create properly formatted Modbus message\nfunction createModbusMsg(value, address) {\n    return {\n        payload: {\n            value: value ? true : false,  // Convert to boolean\n            fc: 5,                        // Force Single Coil\n            unitid: 1,                    // Modbus unit ID\n            address: address,             // Coil address (0-based)\n            quantity: 1                   // Always 1 for single coil\n        }\n    };\n}\n\nlet currentZone = 0;\nlet isIrrigating = false;\nlet zoneEndTime = null;\n\n// Function to turn off a zone\nfunction turnOffZone(zoneIndex) {\n    const modbusMsg = createModbusMsg(false, zones[zoneIndex].channel - 1);\n    \n    // Add zone info for debugging\n    modbusMsg.zoneInfo = {\n        name: zones[zoneIndex].name,\n        channel: zones[zoneIndex].channel,\n        action: 'OFF'\n    };\n    \n    return modbusMsg;\n}\n\n// Function to turn on a zone\nfunction turnOnZone(zoneIndex, duration) {\n    const modbusMsg = createModbusMsg(true, zones[zoneIndex].channel - 1);\n    \n    // Add zone info for debugging\n    modbusMsg.zoneInfo = {\n        name: zones[zoneIndex].name,\n        channel: zones[zoneIndex].channel,\n        duration: duration,\n        action: 'ON'\n    };\n    \n    return modbusMsg;\n}\n\n// Main flow logic\nfunction processIrrigationCycle() {\n    const now = new Date();\n    \n    // If we're currently irrigating, check if the current zone needs to be turned off\n    if (isIrrigating && zoneEndTime && now >= new Date(zoneEndTime)) {\n        // Turn off current zone\n        node.send([turnOffZone(currentZone), null]);\n        \n        // Move to the next zone\n        currentZone = (currentZone + 1) % zones.length;\n        \n        // Reset irrigation if we've completed all zones\n        if (currentZone === 0) {\n            isIrrigating = false;\n            zoneEndTime = null;\n        } else {\n            // Calculate adjusted watering time for the new zone\n            const adjustedTime = Math.round(msg.payload.wateringTime * zones[currentZone].adjustmentFactor);\n            \n            // Set end time for the new zone\n            zoneEndTime = new Date(now.getTime() + (adjustedTime * 60 * 1000));\n            \n            // Turn on the new zone\n            node.send([turnOnZone(currentZone, adjustedTime), null]);\n        }\n        \n        // Update context\n        context.set('currentZone', currentZone);\n        context.set('isIrrigating', isIrrigating);\n        context.set('zoneEndTime', zoneEndTime);\n    }\n    \n    // If we're not currently irrigating, check if we have a water need\n    if (!isIrrigating && msg.payload && msg.payload.wateringTime > 0) {\n        isIrrigating = true;\n        \n        // Calculate adjusted watering time for the first zone\n        const adjustedTime = Math.round(msg.payload.wateringTime * zones[currentZone].adjustmentFactor);\n        \n        // Set end time for the first zone\n        zoneEndTime = new Date(now.getTime() + (adjustedTime * 60 * 1000));\n        \n        // Turn on the first zone\n        node.send([turnOnZone(currentZone, adjustedTime), null]);\n        \n        // Update context\n        context.set('isIrrigating', isIrrigating);\n        context.set('zoneEndTime', zoneEndTime);\n    }\n    \n    // Send status to second output\n    const status = {\n        payload: {\n            currentZone: currentZone,\n            zoneName: zones[currentZone].name,\n            isIrrigating: isIrrigating,\n            endTime: zoneEndTime,\n            timestamp: now.toISOString()\n        }\n    };\n    \n    // First output is Modbus message (or null), second output is status\n    node.send([null, status]);\n}\n\n// Call the main flow logic\nprocessIrrigationCycle();","outputs":2,"timeout":"","noerr":0,"initialize":"// Initialize context variables\ncontext.set('currentZone', 0);\ncontext.set('isIrrigating', false);\ncontext.set('zoneEndTime', null);","finalize":"// Clean up on deploy\nif (context.get('isIrrigating')) {\n    // Turn off current zone\n    const currentZone = context.get('currentZone') || 0;\n    node.send({\n        payload: {\n            'value': 0,\n            'fc': 5,\n            'address': currentZone\n        }\n    });\n}","libs":[],"x":1370,"y":920,"wires":[["c541f94e9d858c5d","70d9add1645d8bee"],["09f6e23668a9d7c3"]]},{"id":"6133e489ccb75478","type":"link in","z":"d0a0029743190dc0","name":"link in 57","links":["f5697da887a54c75"],"x":785,"y":860,"wires":[["9724eb3843657c92"]]},{"id":"37d2ade2d9c5c4b2","type":"debug","z":"d0a0029743190dc0","name":"debug 19","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1300,"y":860,"wires":[]},{"id":"70d9add1645d8bee","type":"debug","z":"d0a0029743190dc0","name":"debug 20","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1500,"y":860,"wires":[]},{"id":"4ca067a6203537e9","type":"modbus-flex-getter","z":"d0a0029743190dc0","name":"","showStatusActivities":false,"showErrors":false,"showWarnings":true,"logIOActivities":false,"server":"relay-server","useIOFile":false,"ioFile":"","useIOForPayload":false,"emptyMsgOnFail":false,"keepMsgProperties":false,"delayOnStart":false,"startDelayTime":"","x":1490,"y":1080,"wires":[["428e9452bc3f7e91"],["516c75035c6001d5"]]},{"id":"428e9452bc3f7e91","type":"debug","z":"d0a0029743190dc0","name":"debug 21","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1740,"y":1040,"wires":[]},{"id":"516c75035c6001d5","type":"debug","z":"d0a0029743190dc0","name":"debug 22","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1740,"y":1120,"wires":[]},{"id":"15f59032e962bf06","type":"function","z":"d0a0029743190dc0","name":"On","func":"msg.payload = {\n    'value' : true,\n    'fc' : 1,\n    'address' : 0,\n    'quantity' : 1\n};\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1250,"y":1080,"wires":[["4ca067a6203537e9"]]},{"id":"a72e058e18383329","type":"debug","z":"d0a0029743190dc0","name":"debug 23","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1880,"y":860,"wires":[]},{"id":"relay-server","type":"modbus-client","name":"Waveshare ETH Relay","clienttype":"tcp","bufferCommands":true,"stateLogEnabled":false,"queueLogEnabled":false,"failureLogEnabled":false,"tcpHost":"192.168.0.200","tcpPort":"502","tcpType":"TCP-RTU-BUFFERED","serialPort":"/dev/ttyUSB","serialType":"RTU-BUFFERED","serialBaudrate":"9600","serialDatabits":"8","serialStopbits":"1","serialParity":"none","serialConnectionDelay":"100","serialAsciiResponseStartDelimiter":"0x3A","unit_id":"1","commandDelay":"1","clientTimeout":"1000","reconnectOnTimeout":true,"reconnectTimeout":"2000","parallelUnitIdsAllowed":true,"showErrors":false,"showWarnings":false,"showLogs":true}]

As reference, restarting the flow, the first two messages from the zone status are:

{"currentZone":0,"zoneName":"Lawn Front","isIrrigating":true,"endTime":"2024-11-09T18:36:12.970Z","timestamp":"2024-11-09T18:30:12.970Z"}

and then after about 33 seconds from the first:

{"currentZone":0,"zoneName":"Lawn Front","isIrrigating":false,"endTime":null,"timestamp":"2024-11-09T18:30:45.881Z"}