Node-Red example flows for beginners

It would be nice to have a thread of simple examples of Node-Red flows interacting with FarmOS. I’ve tried this a few times myself, but I never get it to the finish.
So I ask for help here, and also try to make it easier for others to start using Node-Red.

@Farmer-Ed has documented a lot of his Node-Red project here in the forum, but it’s quite complex to start with.

I think a screenshot of the Flow, and a JSON export of the flow could work.
The flows shold not be farm specific.

Authenticate with FarmOS:


This flow should provide a login to FarmOS. It also keeps you logged in for some time.
It would need the installation of node-red-dashboard and maybe node-red-contrib-oauth2
Edit: node-red-contrib-credentials is also needed.

image

The image node must be setup with the FarmOS URL of your instance.

Then you should be able to log in from the dashboard in Node Red.
If success you will get a notice of “Access Granted”
image

[
    {
        "id": "cb009280587b2d34",
        "type": "tab",
        "label": "FarmOS authenticate",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "54e5e55c099a85e8",
        "type": "function",
        "z": "cb009280587b2d34",
        "name": "Save OAuth Token",
        "func": "if (msg.payload.access_token) {\n    var token = msg.payload.access_token;\n    var refresh = msg.payload.refresh_token;\n    global.set('token', token);\n    flow.set('refresh', refresh);\n    Time = new Date()\n    msg.payload = \"Access Granted \" + Time;\n    \n} else {\n    msg.payload = \"Authorization error.\";\n}\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "x": 850,
        "y": 80,
        "wires": [
            [
                "553b16d833bcfc85"
            ]
        ]
    },
    {
        "id": "8439cae9548df4ae",
        "type": "function",
        "z": "cb009280587b2d34",
        "name": "Refresh Token",
        "func": "var refresh = flow.get('refresh');\n\nmsg.oauth2Request = {\n    \"access_token_url\": global.get('farmOSurl').concat('/oauth/token'), \n    \"credentials\": {\n        \"grant_type\": \"refresh_token\",\n        \"client_id\": \"farm\",\n        \"client_secret\": \"client_secret\",\n        \"refresh_token\": refresh,\n        \"scope\": \"farm_manager\"\n    }\n};\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "x": 500,
        "y": 20,
        "wires": [
            [
                "7aa71dad7ad382af"
            ]
        ]
    },
    {
        "id": "814dda54a6d2dfcb",
        "type": "function",
        "z": "cb009280587b2d34",
        "name": "Creat OAuth2Request",
        "func": "var farmOSurl = msg.payload.url\nglobal.set('farmOSurl', farmOSurl);\n\nmsg.oauth2Request = { \n        \"access_token_url\": msg.payload.url.concat(\"/oauth/token\"), \n        \"credentials\": {\n            \"grant_type\": \"password\",\n            \"username\": msg.payload.username,\n            \"password\": msg.payload.password,\n            \"client_id\": \"farm\",\n            \"client_secret\": \"client_secret\",\n            \"scope\": \"farm_manager\"\n        }\n    };\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "x": 480,
        "y": 80,
        "wires": [
            [
                "7aa71dad7ad382af"
            ]
        ]
    },
    {
        "id": "7aa71dad7ad382af",
        "type": "oauth2",
        "z": "cb009280587b2d34",
        "name": "",
        "container": "payload",
        "access_token_url": "http://localhost/oauth2/token",
        "grant_type": "set_by_credentials",
        "username": "",
        "password": "",
        "client_id": "",
        "client_secret": "",
        "scope": "farmos_restws_access",
        "rejectUnauthorized": true,
        "headers": {},
        "x": 670,
        "y": 80,
        "wires": [
            [
                "54e5e55c099a85e8"
            ]
        ]
    },
    {
        "id": "01598c1320769cdd",
        "type": "credentials",
        "z": "cb009280587b2d34",
        "name": "farmOS URL ",
        "props": [
            {
                "value": "payload.url",
                "type": "msg"
            }
        ],
        "credentials": {},
        "x": 270,
        "y": 80,
        "wires": [
            [
                "814dda54a6d2dfcb"
            ]
        ]
    },
    {
        "id": "beb68fa523b60517",
        "type": "ui_button",
        "z": "cb009280587b2d34",
        "name": "",
        "group": "16448850.68666",
        "order": 2,
        "width": 0,
        "height": 0,
        "passthru": false,
        "label": "Refresh Token",
        "tooltip": "",
        "color": "",
        "bgcolor": "",
        "className": "",
        "icon": "",
        "payload": "",
        "payloadType": "str",
        "topic": "topic",
        "topicType": "msg",
        "x": 90,
        "y": 20,
        "wires": [
            [
                "8439cae9548df4ae"
            ]
        ]
    },
    {
        "id": "269fc9d8c70219ea",
        "type": "ui_form",
        "z": "cb009280587b2d34",
        "name": "",
        "label": "FarmOS Login",
        "group": "16448850.68666",
        "order": 1,
        "width": 0,
        "height": 0,
        "options": [
            {
                "label": "Username",
                "value": "username",
                "type": "text",
                "required": true,
                "rows": null
            },
            {
                "label": "Password",
                "value": "password",
                "type": "password",
                "required": true,
                "rows": null
            }
        ],
        "formValue": {
            "username": "",
            "password": ""
        },
        "payload": "",
        "submit": "Logg inn",
        "cancel": "Avbryt",
        "topic": "",
        "topicType": "str",
        "splitLayout": false,
        "className": "",
        "x": 90,
        "y": 80,
        "wires": [
            [
                "01598c1320769cdd"
            ]
        ]
    },
    {
        "id": "553b16d833bcfc85",
        "type": "ui_text",
        "z": "cb009280587b2d34",
        "group": "16448850.68666",
        "order": 2,
        "width": 0,
        "height": 0,
        "name": "",
        "label": "Auth Token",
        "format": "{{msg.payload}}",
        "layout": "row-spread",
        "className": "",
        "x": 1050,
        "y": 80,
        "wires": []
    },
    {
        "id": "16448850.68666",
        "type": "ui_group",
        "name": "",
        "tab": "e4996a8c673daf28",
        "order": 2,
        "disp": true,
        "width": "6",
        "collapse": false,
        "className": ""
    },
    {
        "id": "e4996a8c673daf28",
        "type": "ui_tab",
        "name": "farmOS OAuth2",
        "icon": "dashboard",
        "order": 1,
        "disabled": false,
        "hidden": false
    }
]

Most of my flows use custom nodes which are not available to install via Manage Palette in Node-Red, but can be installed by running the following inside the Node-Red folder.

npm i @farmer-ed/node-red-4-farmos

Authentication

[
    {
        "id": "1cc68abbf2c0569c",
        "type": "inject",
        "z": "01518f98870b2ad5",
        "name": "",
        "props": [],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": "10",
        "topic": "",
        "x": 170,
        "y": 120,
        "wires": [
            [
                "aa45d743bb90f4ab"
            ]
        ]
    },
    {
        "id": "ac596d264a467b63",
        "type": "debug",
        "z": "01518f98870b2ad5",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": true,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "payload",
        "statusType": "auto",
        "x": 750,
        "y": 120,
        "wires": []
    },
    {
        "id": "4f3a447ade862043",
        "type": "comment",
        "z": "01518f98870b2ad5",
        "name": "Oauth2 example",
        "info": "",
        "x": 180,
        "y": 60,
        "wires": []
    },
    {
        "id": "aa45d743bb90f4ab",
        "type": "credentials",
        "z": "01518f98870b2ad5",
        "name": "farmOS Credentials",
        "props": [
            {
                "value": "payload.url",
                "type": "msg"
            },
            {
                "value": "payload.username",
                "type": "msg"
            },
            {
                "value": "payload.password",
                "type": "msg"
            },
            {
                "value": "payload.client_id",
                "type": "msg"
            },
            {
                "value": "client_secret",
                "type": "msg"
            },
            {
                "value": "payload.scope",
                "type": "msg"
            }
        ],
        "credentials": {},
        "x": 350,
        "y": 120,
        "wires": [
            [
                "5fc86eeb94e10cb0"
            ]
        ]
    },
    {
        "id": "5fc86eeb94e10cb0",
        "type": "sf:895a366cbeeeb85e",
        "z": "01518f98870b2ad5",
        "name": "",
        "credentials": {},
        "x": 560,
        "y": 120,
        "wires": [
            [
                "ac596d264a467b63"
            ]
        ]
    }
]

Simple GET
image
image

[
    {
        "id": "d1bdb8fba4d14229",
        "type": "comment",
        "z": "01518f98870b2ad5",
        "name": "Get Examples",
        "info": "",
        "x": 170,
        "y": 200,
        "wires": []
    },
    {
        "id": "f7be6a3510df7551",
        "type": "inject",
        "z": "01518f98870b2ad5",
        "name": "",
        "props": [],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "x": 170,
        "y": 260,
        "wires": [
            [
                "69ef18606737bae9"
            ]
        ]
    },
    {
        "id": "69ef18606737bae9",
        "type": "sf:949541c9b70110d5",
        "z": "01518f98870b2ad5",
        "name": "",
        "API": "log/transplanting",
        "Method": "GET",
        "x": 330,
        "y": 260,
        "wires": [
            [
                "f18dbdfa68fe209f"
            ]
        ]
    },
    {
        "id": "f18dbdfa68fe209f",
        "type": "debug",
        "z": "01518f98870b2ad5",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": true,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "payload",
        "statusType": "auto",
        "x": 510,
        "y": 260,
        "wires": []
    }
]
1 Like

GET taxonomy terms and save to a flow variable array
If paginated all results are combined into a single array in Node-Red. Array is destroyed and recreated on each run.

[
    {
        "id": "96151bcfe0fc47aa",
        "type": "inject",
        "z": "01518f98870b2ad5",
        "name": "",
        "props": [],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "x": 170,
        "y": 320,
        "wires": [
            [
                "7b059f30417e29b4"
            ]
        ]
    },
    {
        "id": "753807fcc66731e5",
        "type": "sf:949541c9b70110d5",
        "z": "01518f98870b2ad5",
        "name": "",
        "API": "taxonomy_term/animal_type",
        "Method": "GET",
        "x": 510,
        "y": 320,
        "wires": [
            [
                "b3d2697f4efe2d5f"
            ]
        ]
    },
    {
        "id": "b3d2697f4efe2d5f",
        "type": "function",
        "z": "01518f98870b2ad5",
        "name": "GET Animal Term ID's",
        "func": "var array = flow.get('breeds') || [];\nvar breed;\nvar id;\n\nmsg.payload.data.forEach(data => {\nbreed = data.attributes.name;\nid = data.id;\narray.push({id:id,breed:breed});\n})\n\nflow.set(\"breeds\", array);\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 720,
        "y": 320,
        "wires": [
            [
                "d1ca3b9a04ba568a"
            ]
        ]
    },
    {
        "id": "d1ca3b9a04ba568a",
        "type": "debug",
        "z": "01518f98870b2ad5",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "payload",
        "statusType": "auto",
        "x": 930,
        "y": 320,
        "wires": []
    },
    {
        "id": "7b059f30417e29b4",
        "type": "change",
        "z": "01518f98870b2ad5",
        "name": "Clear array",
        "rules": [
            {
                "t": "delete",
                "p": "breeds",
                "pt": "flow"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 330,
        "y": 320,
        "wires": [
            [
                "753807fcc66731e5"
            ]
        ]
    }
]

CSV Export

[
    {
        "id": "b5f0fab209322c18",
        "type": "function",
        "z": "01518f98870b2ad5",
        "name": "CSV Headings",
        "func": "//Headings for Creation of Blank CSV File\nmsg.payload = \"Tag Number,Gender,Date of Birth,Breed\";\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 340,
        "y": 540,
        "wires": [
            [
                "af52c3f7785bbd99"
            ]
        ]
    },
    {
        "id": "af52c3f7785bbd99",
        "type": "file",
        "z": "01518f98870b2ad5",
        "name": "Blank File with Headings",
        "filename": "/Node Red/Herd Export.csv",
        "appendNewline": true,
        "createDir": false,
        "overwriteFile": "true",
        "encoding": "none",
        "x": 570,
        "y": 540,
        "wires": [
            [
                "11faaec3fb80cce5"
            ]
        ]
    },
    {
        "id": "f14d58aa9952ef30",
        "type": "function",
        "z": "01518f98870b2ad5",
        "name": "Parse Asset",
        "func": "//Declare Variables\nvar array = flow.get('breeds')\nlet outputMsgs = [];\nvar tag,dob,breed,gender,csv,code;\n\n//extract data from each asset\nmsg.payload.data.forEach(data => {\n    if (data.attributes.archived == null) {\n    tag = data.attributes.id_tag[0].id;\n    gender = data.attributes.sex;\n    dob = data.attributes.birthdate;\n    \n    //Start DOB Date formatting\n    const date = new Date(dob);\n    const newDateString = date.getDate() + \"/\" + (date.getMonth()+1) + \"/\" + date.getFullYear();\n    //End DOB Date formatting\n    \n    //Get Breed from taxanomy UUID\n    breed = data.relationships.animal_type.data.id;\n    array.forEach(dat => {\n    if (dat.id == breed) {\n        code = dat.breed;\n        }\n    })\n    \n    //output Data to CSV one asset at a time\n    csv = tag +\",\"+gender+ \",\" + newDateString + \",\" + code ;   \n    outputMsgs.push({payload:csv});\n    \n    }\n    })\n\nreturn [ outputMsgs ];",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 970,
        "y": 540,
        "wires": [
            [
                "79f6c5a353cb3aef"
            ]
        ]
    },
    {
        "id": "79f6c5a353cb3aef",
        "type": "file",
        "z": "01518f98870b2ad5",
        "name": "Export CSV file",
        "filename": "/Node Red/Herd Export.csv",
        "appendNewline": true,
        "createDir": false,
        "overwriteFile": "false",
        "encoding": "none",
        "x": 1160,
        "y": 540,
        "wires": [
            [
                "d851e8a3f77105ee"
            ]
        ]
    },
    {
        "id": "11faaec3fb80cce5",
        "type": "sf:949541c9b70110d5",
        "z": "01518f98870b2ad5",
        "name": "GET Asset",
        "API": "asset/animal",
        "Method": "GET",
        "x": 790,
        "y": 540,
        "wires": [
            [
                "f14d58aa9952ef30"
            ]
        ]
    },
    {
        "id": "32788bf611c63b3c",
        "type": "inject",
        "z": "01518f98870b2ad5",
        "name": "",
        "props": [],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "x": 170,
        "y": 540,
        "wires": [
            [
                "b5f0fab209322c18"
            ]
        ]
    },
    {
        "id": "d851e8a3f77105ee",
        "type": "debug",
        "z": "01518f98870b2ad5",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "statusVal": "",
        "statusType": "auto",
        "x": 1350,
        "y": 540,
        "wires": []
    }
]

Basic example of POSTing data to FarmoOS:
Provided youre already authorized, this flow will post a plain and simple activity log tied to an equipment asset.

If you set the debug node to view the complete message object, you will get some status message.
image

If success, you will get statuscode 201
image

[
    {
        "id": "2d39822504e03b5b",
        "type": "tab",
        "label": "forum",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "9cdf92a9f8583ac2",
        "type": "change",
        "z": "2d39822504e03b5b",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "{\"data\":{\"type\":\"log--activity\",\"attributes\":{\"name\":\"This is an activity log\",\"status\":\"done\",\"notes\":\"Fra nodered\"},\"relationships\":{\"asset\":{\"data\":{\"type\":\"asset--equipment\",\"id\":\"14c07f0e-84d0-4ac1-8bc5-e9bd5b7cbd4d\"}}}}}",
                "tot": "json"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 340,
        "y": 120,
        "wires": [
            [
                "01661044537a78d6"
            ]
        ]
    },
    {
        "id": "01661044537a78d6",
        "type": "sf:949541c9b70110d5",
        "z": "2d39822504e03b5b",
        "name": "",
        "API": "log/activity",
        "Method": "POST",
        "x": 530,
        "y": 120,
        "wires": [
            [
                "3d7be814bfa50091"
            ]
        ]
    },
    {
        "id": "ff32f244a2986306",
        "type": "inject",
        "z": "2d39822504e03b5b",
        "name": "",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 160,
        "y": 120,
        "wires": [
            [
                "9cdf92a9f8583ac2"
            ]
        ]
    },
    {
        "id": "3d7be814bfa50091",
        "type": "debug",
        "z": "2d39822504e03b5b",
        "name": "debug 36",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "statusVal": "",
        "statusType": "auto",
        "x": 700,
        "y": 120,
        "wires": []
    }
]
1 Like

A basic log creation in Node-Red.
I just push a button on the dashboard, and an activity log of adding salt to an asset is created.

It’s a bit confusing not having a status message in the end, telling me if it was really saved.
So I came up with this solution:

It adds a notification node. It recieves a html string.
It could also be placed after the button node to let the user proceed or abort before saving anything.

bilde
Translation :
“Saved in FarmOS”
The URL takes me to the logs of this asset.

Ideally the URL should point direcly to the logs edit page. But that’s next level.

The switch node checks if msg.statuscode is 201 (success) or not 201.

1 Like

Create observation log by sending email.

bilde

The flow checks for unread mails in a inbox folder. If so, it creates a log with the mail topic as the log’s name, and the mail body as the log’s note.
I also put an “Email” category to it, so it’s easy to verify them later.

I set up a mail alias and when I forward a mail to the alias, the mail is placed in the FarmOS folder.
When the flow is executed, the email node changes the mail read status.

[
    {
        "id": "9d8a923241677a34",
        "type": "e-mail in",
        "z": "1ca30f9571004237",
        "name": "",
        "protocol": "IMAP",
        "server": "imap.fastmail.com",
        "useSSL": true,
        "autotls": "never",
        "port": "993",
        "box": "FarmOS",
        "disposition": "Read",
        "criteria": "UNSEEN",
        "repeat": "3600",
        "fetch": "auto",
        "inputs": 0,
        "x": 190,
        "y": 1080,
        "wires": [
            [
                "e0938f721fea4f15"
            ]
        ]
    },
    {
        "id": "e0938f721fea4f15",
        "type": "function",
        "z": "1ca30f9571004237",
        "name": "function 28",
        "func": "var mail=\n{\n    \"data\": {\n        \"type\": 'log--observation',\n            \"attributes\": {\n            \"name\": \"Epost - \" + msg.topic,\n            \"status\": \"done\",\n            \"notes\": \"\" + msg.payload\n        },\n        \"relationships\": {\n            \"category\": {\n                \"data\": [\n                    {\n                        \"type\": \"taxonomy_term--log_category\",\n                        \"id\": \"dbb44cda-addb-4c5e-b41d-237691a3a71e\" //Epost-katoegori\n                    }\n                ]\n            }\n        }\n    }\n}\nmsg.payload = mail;\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 330,
        "y": 1080,
        "wires": [
            [
                "2894caf76e2466f5"
            ]
        ]
    },
    {
        "id": "2894caf76e2466f5",
        "type": "sf:949541c9b70110d5",
        "z": "1ca30f9571004237",
        "name": "",
        "API": "log/observation",
        "Method": "POST",
        "x": 490,
        "y": 1080,
        "wires": [
            [
                "f8f6784bbb8f464c"
            ]
        ]
    },
    {
        "id": "f8f6784bbb8f464c",
        "type": "debug",
        "z": "1ca30f9571004237",
        "name": "debug 96",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 640,
        "y": 1080,
        "wires": []
    }
]
2 Likes