[ANNOUNCE] Node RED BARRED

I wonder whether you can access the RFID reader on mobile ... hm, would definitely be interesting to go to stores that use RFID and randomly read them! :wink: I do have a flipper that can do that but it's a pain to use - sorry to say.

1 Like

Sure can....

As my language of choice (.NET Maui) gets AoT compiled (cough cough Apple) - the development stack has libs to interact with its API's - it differs of course between the Platforms (Apple, Android) - all the plumbing is there to add RFID support, just need to understand the platform specific layers which are unavoidable.

some resource for good measure : Reading and Writing RFID on Android Using .NET MAUI | by Anto Haryanto | Medium.

@gregorius - I of course, don't know your wing span, but given you explorative approach (Yes! I been watching :wink:), feel free to look at the source of the Native App (.NET Maui)

And it would be cool, to add RFID support (the repo has the Node RED Node counterpart also)

1 Like

FYI - if anyone is thinking about branding Node-RED BARRED with their own logo on the Title Bar at the top of the screen - the size that works best is a '.png' 80 by 80 pixels.

2 Likes

FYI - just a reminder that emojis can be used in the menu structure and text responses.

Just replace lines 7 to 14 with this code in the 'LSM' function node.

    msg.payload = {
        '📋 List items': { action: 'L' },
        '➕ Add new': { action: 'A', scan: true },
        '⏳ Check expiry': { action: 'C' },
        '🗑️ Delete item': { action: 'D', destructive: true, scan: true }
    };
    return [msg, null]
}
2 Likes

Haven't heard that before "wing span" - I like it!

But you know me, I'll get to it once I need it[1] and besides, I've done app development (back on an iPhone 3 or something) and so I've kind of through with it.

Btw did you use handbrake (as I learnt this week) to reduce the size of video - I just started downloading you 200MB+ video and thought better of it....

That gives me the shivers - should I be worried about the cameras in my life ... :wink:

[1] = which does remind of clamp mode (message tracer) - big shout out to @AllanOricil for the suggestion

clampmode

I've improved it so that you can highlight nodes or wires or groups of nodes. It updates with each new selection which makes it super simple to use and hence also there is a danger of flooding the NR websocket with messages.

2 Likes

I knew emojis were possible in the terminal - but having them in the menu is a new one to me :nerd_face:

1 Like

I didn't - I used the raw output from OBS - I know of handbrake, but I rarely do this kind of documentation

Ah ok, OBS might be better than DaVinci which took two of my 150MB videos and blew them up to 900MB. I threw it at handbrake and got 120MB out again!

I can't recommend DaVinci highly enough for good software to edit these demo videos (I'm not complaining about your video here Marcus, just generally ;)). The free version is plenty to clean up videos or create fade-in and fade-outs, title screens, zoom effects ... etc

1 Like

Hmm, I suspect that Mrs TotallyInformation might just get a little upset - definitely not WAF compatible!

2 Likes

They are just characters at the end of the day. So as long as whatever you are using supports UTF-8, you should be good. I use them now extensively in JavaScript and the web.

1 Like

I currently use Kdenlive to create my videos which is super simple to use. I have DaVinci installed though and will doubtless start using it when I have more time to learn it. I do have HandBreak to hand as well but generally don't need it as I have my Kdenlive outputs set to YouTube output sizing already.

I save the OBS output to a video file (or several if I've messed up mid recording!) and then add headings, background music and do edits in Kdenlive. Then upload to YT and add chapters, descriptions, cards, etc.

I also use ScreenToGif when creating quick animated screen captures. It is really good at minimising the captures and outputting to an animated gif format (tiny).

1 Like

@marcus-j-davies Just having a play with this now, nice work !

So I found https://world.openfoodfacts.org/api/v0/product/5000157066312.json as a place to find food item barcode info, (far too lazy to type things in myself).

The above returns a large json object for the barcode, with everything you could want to know, it even has pictures :wink:

Just putting together a quick flow to feed barcodes from your node to http and back to scanner.

Here we go, test bar code if you don't have one to hand -

and it works :wink:

[{"id":"ef2e5c74ebd8741b","type":"barred-barcode","z":"82f803ac88658488","name":"Incoming Barcodes","stack":"a35da90a91fefaae","x":310,"y":320,"wires":[["b25f604dd54c38c9"]]},{"id":"ec4f65e58ec52ac8","type":"barred-result","z":"82f803ac88658488","name":"Send Result","defaultStatus":"OK","x":990,"y":320,"wires":[["6e270b76aed10b13"]]},{"id":"6e270b76aed10b13","type":"debug","z":"82f803ac88658488","name":"debug 93","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1150,"y":320,"wires":[]},{"id":"e6c73935f852f67d","type":"function","z":"82f803ac88658488","name":"Return some fields","func":"if (msg.payload.status===0) {\n    msg.status = \"ERROR\"\n    return msg\n}\n\nconst p = msg.payload.product\n\nlet data = {\n    name: p.product_name_en,\n    brand: p.brands,\n    serving_size: p.serving_size,\n    product_quantity: p.quantity,\n    packaging: p.packaging,\n    ingredients: p.ingredients_text_en\n}\n\nmsg.payload = data\n msg.status = \"OK\"\nreturn msg","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":800,"y":320,"wires":[["ec4f65e58ec52ac8"]]},{"id":"ca1ec77b5e667517","type":"http request","z":"82f803ac88658488","name":"","method":"GET","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":610,"y":320,"wires":[["e6c73935f852f67d"]]},{"id":"b25f604dd54c38c9","type":"function","z":"82f803ac88658488","name":"URL","func":"msg.url = `https://world.openfoodfacts.org/api/v0/product/${msg.payload.barcode.barcode}.json`\nreturn msg","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":470,"y":320,"wires":[["ca1ec77b5e667517"]]},{"id":"a35da90a91fefaae","type":"barred-config","name":"BARRED Config","department":"test","color":"#e49191","rate":1000,"rtimeout":500,"scanners":{"3baab4bf-b325-4f36-95da-1c8a83c626aa":{"index":0,"scannerLabel":"Seans Phone","scannerId":"3baab4bf-b325-4f36-95da-1c8a83c626aa"}},"host":"192.168.0.245","port":51284},{"id":"290f66b7199f9394","type":"global-config","env":[],"modules":{"@marcus-j-davies/node-red-barred":"1.1.2"}}]
3 Likes

HaHa!

Very cool - and good call just to return the error object as it is.

If you were documenting food purchases:
You can present an input form, pre-filling most of the values, whilst leaving a few properties for user input.

msg.status = 'INFO'
msg.payload = {
    barcode: msg._originalPayload.barcode.barcode, /* _originalPayload : Example */
    product_name: msg.payload.product_name
    serving_quantity: `${msg.payload.serving_quantity}${msg.payload.serving_quantity_unit}`
    ...Others

    qty_purchased: 'number',
    date_of_purchase: 'date'
}

@dynamicdave might be interested in that API

2 Likes

That's very useful - I'll take a more detailed look at it later.

When I first starting helping Marcus with testing of BARRED I found quite a few sites that offered a sort of 'table lookup'. Most of them limited the number of times you could access the API before they promted you to sign-up for a paid subscription. This one, which I did breifly look at, doesn't appear to impose a limit.

I'll have a go at integrating into the 'Spice Monitor' as that would improve its functionality and also reduce the amount of user input needed (to maybe just entering the expiry date).

2 Likes

Wow, loads of interesting (and probably generally unimportant!) info returned. Good find.

Just had a go at incorporating the API food-stuff look-up. See notes at bottom of post.

[{"id":"spices_flow","type":"tab","label":"Spice Tracker (BARRED)","disabled":false,"info":""},{"id":"70360db4e5317c10","type":"group","z":"spices_flow","name":"","style":{"fill":"#ffbfbf","label":true},"nodes":["6164d849e91285a1","b2ca26bd9363230f","33d6018d69053bfa","398bbdb5714fcbf1","a1dab0ec604a7010","585a6d354058f3ac","876a3389a414240f","2222073ace8856d8","91dd11d16aca6da2","b0e3ee6cb75aeba9"],"x":54,"y":139,"w":722,"h":302},{"id":"8955cf2843bd4469","type":"group","z":"spices_flow","name":"","style":{"fill":"#e3f3d3","label":true},"nodes":["sqlite_node","debug_out","a5f931c68b82a703","cad5db41fc8a40f9","1f01515d56b50e5a","15ae5f0567797614","3022635ec3ba0ad4","083a74ebdc9372e0","0e215495b8dcee17","a33316d9b35d7d78","6e1546f86e0e0477","c3cfd5458e5ac253","f2b82b8ff60eb8f0","4e6e60955798706e","ec6ee194f932168b","0afe54bf0b9fcbda","d9dc8ff22b702143","7be8c66a940200d9"],"x":34,"y":519,"w":1012,"h":382},{"id":"sqlite_node","type":"sqlite","z":"spices_flow","g":"8955cf2843bd4469","mydb":"sqlite_config","sqlquery":"msg.topic","sql":"","name":"SQLite Spices","x":680,"y":720,"wires":[["6e1546f86e0e0477","debug_out"]]},{"id":"debug_out","type":"debug","z":"spices_flow","g":"8955cf2843bd4469","name":"Output","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":850,"y":780,"wires":[]},{"id":"6164d849e91285a1","type":"function","z":"spices_flow","g":"70360db4e5317c10","name":"SELECT * FROM spices ORDER by name","func":"msg.topic = `SELECT * FROM spices ORDER BY name`;\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":430,"y":340,"wires":[["b0e3ee6cb75aeba9"]]},{"id":"b2ca26bd9363230f","type":"inject","z":"spices_flow","g":"70360db4e5317c10","name":"SELECT","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"num","x":160,"y":340,"wires":[["6164d849e91285a1"]]},{"id":"a5f931c68b82a703","type":"barred-action","z":"spices_flow","g":"8955cf2843bd4469","name":"Incoming Actions","stack":"5eb95b3f58db0bdd","x":140,"y":660,"wires":[["cad5db41fc8a40f9","ec6ee194f932168b"]]},{"id":"cad5db41fc8a40f9","type":"debug","z":"spices_flow","g":"8955cf2843bd4469","name":"Incoming","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":320,"y":620,"wires":[]},{"id":"1f01515d56b50e5a","type":"barred-result","z":"spices_flow","g":"8955cf2843bd4469","name":"Send Results to scanner","defaultStatus":"OK","x":910,"y":640,"wires":[[]]},{"id":"15ae5f0567797614","type":"barred-item","z":"spices_flow","g":"8955cf2843bd4469","name":"Incoming Items","stack":"5eb95b3f58db0bdd","x":140,"y":800,"wires":[["3022635ec3ba0ad4"]]},{"id":"3022635ec3ba0ad4","type":"switch","z":"spices_flow","g":"8955cf2843bd4469","name":"","property":"menu_option","propertyType":"flow","rules":[{"t":"eq","v":"A","vt":"str"},{"t":"eq","v":"D","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":310,"y":800,"wires":[["083a74ebdc9372e0","c3cfd5458e5ac253"],["0e215495b8dcee17"]]},{"id":"083a74ebdc9372e0","type":"debug","z":"spices_flow","g":"8955cf2843bd4469","name":"A","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":470,"y":820,"wires":[]},{"id":"0e215495b8dcee17","type":"debug","z":"spices_flow","g":"8955cf2843bd4469","name":"D","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":470,"y":860,"wires":[]},{"id":"a33316d9b35d7d78","type":"comment","z":"spices_flow","g":"8955cf2843bd4469","name":"First stab at Spice Monitor","info":"","x":170,"y":560,"wires":[]},{"id":"fc068410fbc9e879","type":"comment","z":"spices_flow","name":"First stab at SPICE RACK for Garry Hayne","info":"","x":200,"y":80,"wires":[]},{"id":"6e1546f86e0e0477","type":"function","z":"spices_flow","g":"8955cf2843bd4469","name":"Format dB results","func":"// Get the length of the results array from the MySQL query\n\nlet sql = msg.topic;\n\n// Make sure msg.topic exists\nif (sql && sql.toUpperCase().includes(\"DELETE\")) {\n    // Extract barcode from SQL string\n    let match = sql.match(/barcode\\s*=\\s*'(\\d+)'/i);\n\n    if (match) {\n        let barcode = match[1];  // e.g. \"88888\"\n        msg.payload = `Item with barcode ${barcode}\\nhas been deleted.`;\n    } else {\n        msg.payload = \"Item deleted (barcode not found).\";\n    }\n    return msg;\n}\n\nelse {\n\n    let length = msg.payload.length;\n\n    node.status({ text: \"Rows returned = \" + length });\n\n    let action = flow.get(\"menu_option\");\n\n    let ap_info = \"\"\n\n    if (action == \"L\") {\n\n        ap_info = \"*** Spice Rack inventory ***\\r\\n\\r\\n\";\n\n        if (length == 0) {\n            ap_info += \"Spice Rack appears to be empty\"\n        }\n\n        // Construct main body of the message\n        for (let row = 0; row < length; row++) {\n            ap_info += \"Name: \" + msg.payload[row].name + \"\\r\\n\";\n            ap_info += \"Barcode: \" + msg.payload[row].barcode + \"\\r\\n\";\n            ap_info += \"Brand: \" + msg.payload[row].brand + \"\\r\\n\";\n            ap_info += \"Quantity: \" + msg.payload[row].quantity + \"\\r\\n\";\n            ap_info += \"Expiry date: \" + msg.payload[row].expiry + \"\\r\\n\\r\\n\";\n        }\n\n        msg.payload = ap_info;\n        return msg;\n    }\n\n    else if (action == \"C\") {\n        ap_info = \"*** Items expiring soon ***\\r\\n\\r\\n\";\n\n        if (length == 0) {\n            ap_info += \"Nothing expiring soon\"\n        }\n\n        // Construct main body of the message\n        for (let row = 0; row < length; row++) {\n            ap_info += \"Name: \" + msg.payload[row].name + \"\\r\\n\";\n            ap_info += \"Barcode: \" + msg.payload[row].barcode + \"\\r\\n\";\n            ap_info += \"Brand: \" + msg.payload[row].brand + \"\\r\\n\";\n            ap_info += \"Quantity: \" + msg.payload[row].quantity + \"\\r\\n\";\n            ap_info += \"Expiry date: \" + msg.payload[row].expiry + \"\\r\\n\\r\\n\";\n        }\n\n        msg.payload = ap_info;\n        return msg;\n\n    }\n}","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":890,"y":720,"wires":[["1f01515d56b50e5a"]]},{"id":"c3cfd5458e5ac253","type":"function","z":"spices_flow","g":"8955cf2843bd4469","name":"INSERT new item","func":"let item = msg.payload.item;\n\n// Prepare SQL query for output 2\nlet sqlMsg = {\n    topic: `INSERT INTO spices (name, brand, quantity, expiry, barcode)\n            VALUES ('${item.Name}', '${item.Brand}', '${item.Quantity}', date(${item.Expiry_date / 1000}, 'unixepoch'), '${item.Barcode}')`\n};\n\n// Prepare confirmation message for output 1\nlet payloadMsg = {\n    payload: `The item ${item.Name}\\nwith barcode ${item.Barcode}\\nhas been stored.`\n};\n\n// Return messages in the order of outputs\nreturn [payloadMsg, sqlMsg];\n","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":490,"y":780,"wires":[["f2b82b8ff60eb8f0","4e6e60955798706e"],["sqlite_node"]]},{"id":"33d6018d69053bfa","type":"function","z":"spices_flow","g":"70360db4e5317c10","name":"Delete all rows in table 'spices'","func":"msg.topic = `DELETE FROM spices`;\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":390,"y":400,"wires":[["b0e3ee6cb75aeba9"]]},{"id":"398bbdb5714fcbf1","type":"inject","z":"spices_flow","g":"70360db4e5317c10","name":"DELETE","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"num","x":160,"y":400,"wires":[["33d6018d69053bfa"]]},{"id":"f2b82b8ff60eb8f0","type":"debug","z":"spices_flow","g":"8955cf2843bd4469","name":"INSERT","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":700,"y":860,"wires":[]},{"id":"a1dab0ec604a7010","type":"comment","z":"spices_flow","g":"70360db4e5317c10","name":"This is for TESTING and creating the 'spices' table on the SQLite dB demo.db","info":"","x":350,"y":180,"wires":[]},{"id":"4e6e60955798706e","type":"barred-send-item","z":"spices_flow","g":"8955cf2843bd4469","name":"Send to Scanners","stack":"5eb95b3f58db0bdd","x":890,"y":820,"wires":[[]]},{"id":"adf9f60103303bea","type":"comment","z":"spices_flow","name":"Added extra 'flow' to confirm delete an item","info":"","x":580,"y":80,"wires":[]},{"id":"876a3389a414240f","type":"function","z":"spices_flow","g":"70360db4e5317c10","name":"CREATE TABLE IF NOT EXISTS spices","func":"msg.topic = `\nCREATE TABLE spices (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    name TEXT,\n    brand TEXT,\n    quantity TEXT,\n    expiry DATE,\n    barcode TEXT\n);\n`;\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":480,"y":280,"wires":[["b0e3ee6cb75aeba9"]]},{"id":"585a6d354058f3ac","type":"inject","z":"spices_flow","g":"70360db4e5317c10","name":"Create 'spices' table","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"num","x":190,"y":280,"wires":[["876a3389a414240f"]]},{"id":"b69bb2bee3a0ddc9","type":"comment","z":"spices_flow","name":"This flow uses SQLite with a database at: /home/pi/demo.db","info":"","x":820,"y":480,"wires":[]},{"id":"ec6ee194f932168b","type":"function","z":"spices_flow","g":"8955cf2843bd4469","name":"Linear Sequence Machine (LSM)","func":"flow.set(\"menu_option\", msg.payload.action.name);\n\nif (msg.payload.action.name == 'DEFAULT') {\n    msg.status = 'MENU'                             // Menu Trigger\n    msg.topic = 'Spices Menu'\n\n    msg.payload = {\n        '📋 List items': { action: 'L' },\n        '➕ Add new': { action: 'A', scan: true },\n        '⏳ Check expiry': { action: 'C' },\n        '🗑️ Delete item': { action: 'D', destructive: true, scan: true }\n    };\n    return [null, msg, null]\n}\n\n\nelse if (msg.payload.action.name == \"L\") {\n    msg.topic = `SELECT * FROM spices ORDER BY name`;\n    return [null, null, msg]\n}\n\nelse if (msg.payload.action.name == \"A\") {\n    msg.barcode = msg.payload.barcode.barcode;\n    msg.url = `https://world.openfoodfacts.org/api/v0/product/${msg.payload.barcode.barcode}.json`\n    return [msg, null, null];\n}\n\nelse if (msg.payload.action.name == \"C\") {\n    msg.topic = \"SELECT * FROM spices WHERE expiry <= date('now', '+7 days') ORDER BY expiry\";\n    return [null, null, msg];\n}\n\nelse if (msg.payload.action.name == \"D\") {\n\n    flow.set(\"barcode\", msg.payload.barcode.barcode);\n\n    msg.status = 'MENU'\n    msg.topic = 'Delete barcode ?'\n    msg.payload = {\n        'Yes': { action: 'Y', scan: false },\n        'No': { action: 'N', scan: false }\n    }\n    return [null, msg, null];\n\n}\n\nelse if (msg.payload.action.name == \"N\") {\n    msg.payload = \"OK - action ignored\";\n    return [null, msg, null];\n}\n\nelse if (msg.payload.action.name == \"Y\") {\n    const barcode = flow.get(\"barcode\");\n    msg.topic = `DELETE FROM spices WHERE barcode='${barcode}'`;\n    return [null, null, msg];\n}\n","outputs":3,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":400,"y":680,"wires":[["0afe54bf0b9fcbda"],["1f01515d56b50e5a"],["sqlite_node"]],"outputLabels":["A option","send_result","sqlite"]},{"id":"7be8c66a940200d9","type":"function","z":"spices_flow","g":"8955cf2843bd4469","name":"Return key fields","func":"const p = msg.payload.product\n\nmsg.status = \"INFO\";\nmsg.payload = {\n    Name: p.product_name_en,\n    Barcode: msg.barcode,\n    Brand: p.brands,\n    Quantity: p.quantity,\n    Expiry_date: 'date'\n};\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":890,"y":560,"wires":[["1f01515d56b50e5a"]]},{"id":"0afe54bf0b9fcbda","type":"http request","z":"spices_flow","g":"8955cf2843bd4469","name":"","method":"GET","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":690,"y":560,"wires":[["7be8c66a940200d9"]]},{"id":"91dd11d16aca6da2","type":"function","z":"spices_flow","g":"70360db4e5317c10","name":"Drop table 'spices'","func":"// Drop the table\nmsg.topic = `DROP TABLE IF EXISTS spices;`;\nreturn msg;\n\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":410,"y":220,"wires":[["b0e3ee6cb75aeba9"]]},{"id":"2222073ace8856d8","type":"inject","z":"spices_flow","g":"70360db4e5317c10","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":220,"wires":[["91dd11d16aca6da2"]]},{"id":"b0e3ee6cb75aeba9","type":"link out","z":"spices_flow","g":"70360db4e5317c10","name":"OUT_testing","mode":"link","links":["d9dc8ff22b702143"],"x":735,"y":220,"wires":[]},{"id":"d9dc8ff22b702143","type":"link in","z":"spices_flow","g":"8955cf2843bd4469","name":"IN_sqlite","links":["b0e3ee6cb75aeba9"],"x":525,"y":560,"wires":[["sqlite_node"]]},{"id":"sqlite_config","type":"sqlitedb","db":"/home/pi/demo.db","mode":"RWC"},{"id":"5eb95b3f58db0bdd","type":"barred-config","name":"BARRED Config","department":"Spice Monitor","color":"#008040","rate":"3000","rtimeout":"1000","scanners":{"646af871-ccf6-476f-b781-283beb1cd691":{"index":0,"scannerLabel":"Scanner A","scannerId":"646af871-ccf6-476f-b781-283beb1cd691"}},"host":"192.168.1.152","port":"50031"}]


Notes:
1: After scanning the item all you need to do is use the calendar to select the 'expiry date'
2: The table structure needs to be changed to include 'brand' column
Use 'DROP' and then 'Create' in the pink coloured part of the flow to alter the table..
3: You will need to replace MY scanner's credentials with YOURS in the config area.

4 Likes

Discovered that some items returned by the food-products API do not have a quantity attribute.
Made some changes to the 'Return key fields' function node to deal with this situation. I also capture barcode in msg.barcode in the LSM (so have attached latest flow below).

// Added check to see if 'quantity' is missing
// If it is, prompt user to enter quantity/size/weight

const p = msg.payload.product

let quantityText = p.quantity;
if (!quantityText || quantityText.trim() === "") {
    quantityText = 'string'; 
}

msg.status = "INFO";
msg.payload = {
    Name: p.product_name_en,
    Barcode: msg.barcode,
    Brand: p.brands,
    Quantity: quantityText,
    Expiry_date: 'date'
};
return msg;

Here's the latest complete 'flow' (as of 14:00hrs on 14/Nov/2025)...

[{"id":"spices_flow","type":"tab","label":"Spice Tracker (BARRED)","disabled":false,"info":""},{"id":"70360db4e5317c10","type":"group","z":"spices_flow","name":"","style":{"fill":"#ffbfbf","label":true},"nodes":["6164d849e91285a1","b2ca26bd9363230f","33d6018d69053bfa","398bbdb5714fcbf1","a1dab0ec604a7010","585a6d354058f3ac","876a3389a414240f","2222073ace8856d8","91dd11d16aca6da2","b0e3ee6cb75aeba9"],"x":54,"y":139,"w":722,"h":302},{"id":"8955cf2843bd4469","type":"group","z":"spices_flow","name":"","style":{"fill":"#e3f3d3","label":true},"nodes":["sqlite_node","debug_out","a5f931c68b82a703","cad5db41fc8a40f9","1f01515d56b50e5a","15ae5f0567797614","3022635ec3ba0ad4","083a74ebdc9372e0","0e215495b8dcee17","a33316d9b35d7d78","6e1546f86e0e0477","c3cfd5458e5ac253","f2b82b8ff60eb8f0","4e6e60955798706e","ec6ee194f932168b","0afe54bf0b9fcbda","d9dc8ff22b702143","7be8c66a940200d9"],"x":34,"y":519,"w":1012,"h":382},{"id":"sqlite_node","type":"sqlite","z":"spices_flow","g":"8955cf2843bd4469","mydb":"sqlite_config","sqlquery":"msg.topic","sql":"","name":"SQLite Spices","x":680,"y":720,"wires":[["6e1546f86e0e0477","debug_out"]]},{"id":"debug_out","type":"debug","z":"spices_flow","g":"8955cf2843bd4469","name":"Output","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":850,"y":780,"wires":[]},{"id":"6164d849e91285a1","type":"function","z":"spices_flow","g":"70360db4e5317c10","name":"SELECT * FROM spices ORDER by name","func":"msg.topic = `SELECT * FROM spices ORDER BY name`;\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":430,"y":340,"wires":[["b0e3ee6cb75aeba9"]]},{"id":"b2ca26bd9363230f","type":"inject","z":"spices_flow","g":"70360db4e5317c10","name":"SELECT","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"num","x":160,"y":340,"wires":[["6164d849e91285a1"]]},{"id":"a5f931c68b82a703","type":"barred-action","z":"spices_flow","g":"8955cf2843bd4469","name":"Incoming Actions","stack":"5eb95b3f58db0bdd","x":140,"y":660,"wires":[["cad5db41fc8a40f9","ec6ee194f932168b"]]},{"id":"cad5db41fc8a40f9","type":"debug","z":"spices_flow","g":"8955cf2843bd4469","name":"Incoming","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":320,"y":620,"wires":[]},{"id":"1f01515d56b50e5a","type":"barred-result","z":"spices_flow","g":"8955cf2843bd4469","name":"Send Results to scanner","defaultStatus":"OK","x":910,"y":640,"wires":[[]]},{"id":"15ae5f0567797614","type":"barred-item","z":"spices_flow","g":"8955cf2843bd4469","name":"Incoming Items","stack":"5eb95b3f58db0bdd","x":140,"y":800,"wires":[["3022635ec3ba0ad4"]]},{"id":"3022635ec3ba0ad4","type":"switch","z":"spices_flow","g":"8955cf2843bd4469","name":"","property":"menu_option","propertyType":"flow","rules":[{"t":"eq","v":"A","vt":"str"},{"t":"eq","v":"D","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":310,"y":800,"wires":[["083a74ebdc9372e0","c3cfd5458e5ac253"],["0e215495b8dcee17"]]},{"id":"083a74ebdc9372e0","type":"debug","z":"spices_flow","g":"8955cf2843bd4469","name":"A","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":470,"y":820,"wires":[]},{"id":"0e215495b8dcee17","type":"debug","z":"spices_flow","g":"8955cf2843bd4469","name":"D","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":470,"y":860,"wires":[]},{"id":"a33316d9b35d7d78","type":"comment","z":"spices_flow","g":"8955cf2843bd4469","name":"First stab at Spice Monitor","info":"","x":170,"y":560,"wires":[]},{"id":"fc068410fbc9e879","type":"comment","z":"spices_flow","name":"First stab at SPICE RACK for Garry Hayne","info":"","x":200,"y":80,"wires":[]},{"id":"6e1546f86e0e0477","type":"function","z":"spices_flow","g":"8955cf2843bd4469","name":"Format dB results","func":"// Get the length of the results array from the MySQL query\n\nlet sql = msg.topic;\n\n// Make sure msg.topic exists\nif (sql && sql.toUpperCase().includes(\"DELETE\")) {\n    // Extract barcode from SQL string\n    let match = sql.match(/barcode\\s*=\\s*'(\\d+)'/i);\n\n    if (match) {\n        let barcode = match[1];  // e.g. \"88888\"\n        msg.payload = `Item with barcode ${barcode}\\nhas been deleted.`;\n    } else {\n        msg.payload = \"Item deleted (barcode not found).\";\n    }\n    return msg;\n}\n\nelse {\n\n    let length = msg.payload.length;\n\n    node.status({ text: \"Rows returned = \" + length });\n\n    let action = flow.get(\"menu_option\");\n\n    let ap_info = \"\"\n\n    if (action == \"L\") {\n\n        ap_info = \"*** Spice Rack inventory ***\\r\\n\\r\\n\";\n\n        if (length == 0) {\n            ap_info += \"Spice Rack appears to be empty\"\n        }\n\n        // Construct main body of the message\n        for (let row = 0; row < length; row++) {\n            ap_info += \"Name: \" + msg.payload[row].name + \"\\r\\n\";\n            ap_info += \"Barcode: \" + msg.payload[row].barcode + \"\\r\\n\";\n            ap_info += \"Brand: \" + msg.payload[row].brand + \"\\r\\n\";\n            ap_info += \"Quantity: \" + msg.payload[row].quantity + \"\\r\\n\";\n            ap_info += \"Expiry date: \" + msg.payload[row].expiry + \"\\r\\n\\r\\n\";\n        }\n\n        msg.payload = ap_info;\n        return msg;\n    }\n\n    else if (action == \"C\") {\n        ap_info = \"*** Items expiring soon ***\\r\\n\\r\\n\";\n\n        if (length == 0) {\n            ap_info += \"Nothing expiring soon\"\n        }\n\n        // Construct main body of the message\n        for (let row = 0; row < length; row++) {\n            ap_info += \"Name: \" + msg.payload[row].name + \"\\r\\n\";\n            ap_info += \"Barcode: \" + msg.payload[row].barcode + \"\\r\\n\";\n            ap_info += \"Brand: \" + msg.payload[row].brand + \"\\r\\n\";\n            ap_info += \"Quantity: \" + msg.payload[row].quantity + \"\\r\\n\";\n            ap_info += \"Expiry date: \" + msg.payload[row].expiry + \"\\r\\n\\r\\n\";\n        }\n\n        msg.payload = ap_info;\n        return msg;\n\n    }\n}","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":890,"y":720,"wires":[["1f01515d56b50e5a"]]},{"id":"c3cfd5458e5ac253","type":"function","z":"spices_flow","g":"8955cf2843bd4469","name":"INSERT new item","func":"let item = msg.payload.item;\n\n// Prepare SQL query for output 2\nlet sqlMsg = {\n    topic: `INSERT INTO spices (name, brand, quantity, expiry, barcode)\n            VALUES ('${item.Name}', '${item.Brand}', '${item.Quantity}', date(${item.Expiry_date / 1000}, 'unixepoch'), '${item.Barcode}')`\n};\n\n// Prepare confirmation message for output 1\nlet payloadMsg = {\n    payload: `The item ${item.Name}\\nwith barcode ${item.Barcode}\\nhas been stored.`\n};\n\n// Return messages in the order of outputs\nreturn [payloadMsg, sqlMsg];\n","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":490,"y":780,"wires":[["f2b82b8ff60eb8f0","4e6e60955798706e"],["sqlite_node"]]},{"id":"33d6018d69053bfa","type":"function","z":"spices_flow","g":"70360db4e5317c10","name":"Delete all rows in table 'spices'","func":"msg.topic = `DELETE FROM spices`;\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":390,"y":400,"wires":[["b0e3ee6cb75aeba9"]]},{"id":"398bbdb5714fcbf1","type":"inject","z":"spices_flow","g":"70360db4e5317c10","name":"DELETE","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"num","x":160,"y":400,"wires":[["33d6018d69053bfa"]]},{"id":"f2b82b8ff60eb8f0","type":"debug","z":"spices_flow","g":"8955cf2843bd4469","name":"INSERT","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":700,"y":860,"wires":[]},{"id":"a1dab0ec604a7010","type":"comment","z":"spices_flow","g":"70360db4e5317c10","name":"This is for TESTING and creating the 'spices' table on the SQLite dB demo.db","info":"","x":350,"y":180,"wires":[]},{"id":"4e6e60955798706e","type":"barred-send-item","z":"spices_flow","g":"8955cf2843bd4469","name":"Send to Scanners","stack":"5eb95b3f58db0bdd","x":890,"y":820,"wires":[[]]},{"id":"adf9f60103303bea","type":"comment","z":"spices_flow","name":"Last edit by djd at 14:00hrs on 14/Nov/2025","info":"","x":590,"y":80,"wires":[]},{"id":"876a3389a414240f","type":"function","z":"spices_flow","g":"70360db4e5317c10","name":"CREATE TABLE IF NOT EXISTS spices","func":"msg.topic = `\nCREATE TABLE spices (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    name TEXT,\n    brand TEXT,\n    quantity TEXT,\n    expiry DATE,\n    barcode TEXT\n);\n`;\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":480,"y":280,"wires":[["b0e3ee6cb75aeba9"]]},{"id":"585a6d354058f3ac","type":"inject","z":"spices_flow","g":"70360db4e5317c10","name":"Create 'spices' table","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"num","x":190,"y":280,"wires":[["876a3389a414240f"]]},{"id":"b69bb2bee3a0ddc9","type":"comment","z":"spices_flow","name":"This flow uses SQLite with a database at: /home/pi/demo.db","info":"","x":820,"y":480,"wires":[]},{"id":"ec6ee194f932168b","type":"function","z":"spices_flow","g":"8955cf2843bd4469","name":"Linear Sequence Machine (LSM)","func":"flow.set(\"menu_option\", msg.payload.action.name);\n\nif (msg.payload.action.name == 'DEFAULT') {\n    msg.status = 'MENU'                             // Menu Trigger\n    msg.topic = 'Spices Menu'\n\n    msg.payload = {\n        '📋 List items': { action: 'L' },\n        '➕ Add new': { action: 'A', scan: true },\n        '⏳ Check expiry': { action: 'C' },\n        '🗑️ Delete item': { action: 'D', destructive: true, scan: true }\n    };\n    return [null, msg, null]\n}\n\n\nelse if (msg.payload.action.name == \"L\") {\n    msg.topic = `SELECT * FROM spices ORDER BY name`;\n    return [null, null, msg]\n}\n\nelse if (msg.payload.action.name == \"A\") {\n    msg.barcode = msg.payload.barcode.barcode;\n    msg.url = `https://world.openfoodfacts.org/api/v0/product/${msg.payload.barcode.barcode}.json`\n    return [msg, null, null];\n}\n\nelse if (msg.payload.action.name == \"C\") {\n    msg.topic = \"SELECT * FROM spices WHERE expiry <= date('now', '+7 days') ORDER BY expiry\";\n    return [null, null, msg];\n}\n\nelse if (msg.payload.action.name == \"D\") {\n\n    flow.set(\"barcode\", msg.payload.barcode.barcode);\n\n    msg.status = 'MENU'\n    msg.topic = 'Delete barcode ?'\n    msg.payload = {\n        'Yes': { action: 'Y', scan: false },\n        'No': { action: 'N', scan: false }\n    }\n    return [null, msg, null];\n\n}\n\nelse if (msg.payload.action.name == \"N\") {\n    msg.payload = \"OK - action ignored\";\n    return [null, msg, null];\n}\n\nelse if (msg.payload.action.name == \"Y\") {\n    const barcode = flow.get(\"barcode\");\n    msg.topic = `DELETE FROM spices WHERE barcode='${barcode}'`;\n    return [null, null, msg];\n}\n","outputs":3,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":400,"y":680,"wires":[["0afe54bf0b9fcbda"],["1f01515d56b50e5a"],["sqlite_node"]],"outputLabels":["A option","send_result","sqlite"]},{"id":"7be8c66a940200d9","type":"function","z":"spices_flow","g":"8955cf2843bd4469","name":"Return key fields","func":"// Added check to see if 'quantity' is missing\n// If it is, prompt user to enter quantity/size/weight\n\nconst p = msg.payload.product\n\nlet quantityText = p.quantity;\nif (!quantityText || quantityText.trim() === \"\") {\n    quantityText = 'string'; \n}\n\nmsg.status = \"INFO\";\nmsg.payload = {\n    Name: p.product_name_en,\n    Barcode: msg.barcode,\n    Brand: p.brands,\n    Quantity: quantityText,\n    Expiry_date: 'date'\n};\nreturn msg;\n\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":890,"y":560,"wires":[["1f01515d56b50e5a"]]},{"id":"0afe54bf0b9fcbda","type":"http request","z":"spices_flow","g":"8955cf2843bd4469","name":"","method":"GET","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":690,"y":560,"wires":[["7be8c66a940200d9"]]},{"id":"91dd11d16aca6da2","type":"function","z":"spices_flow","g":"70360db4e5317c10","name":"Drop table 'spices'","func":"// Drop the table\nmsg.topic = `DROP TABLE IF EXISTS spices;`;\nreturn msg;\n\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":410,"y":220,"wires":[["b0e3ee6cb75aeba9"]]},{"id":"2222073ace8856d8","type":"inject","z":"spices_flow","g":"70360db4e5317c10","name":"DROP 'spices' table","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"num","x":190,"y":220,"wires":[["91dd11d16aca6da2"]]},{"id":"b0e3ee6cb75aeba9","type":"link out","z":"spices_flow","g":"70360db4e5317c10","name":"OUT_testing","mode":"link","links":["d9dc8ff22b702143"],"x":735,"y":220,"wires":[]},{"id":"d9dc8ff22b702143","type":"link in","z":"spices_flow","g":"8955cf2843bd4469","name":"IN_sqlite","links":["b0e3ee6cb75aeba9"],"x":525,"y":560,"wires":[["sqlite_node"]]},{"id":"sqlite_config","type":"sqlitedb","db":"/home/pi/demo.db","mode":"RWC"},{"id":"5eb95b3f58db0bdd","type":"barred-config","name":"BARRED Config","department":"Spice Monitor","color":"#008040","rate":"3000","rtimeout":"1000","scanners":{"646af871-ccf6-476f-b781-283beb1cd691":{"index":0,"scannerLabel":"Scanner A","scannerId":"646af871-ccf6-476f-b781-283beb1cd691"}},"host":"192.168.1.152","port":"50031"}]
1 Like

Yes there are so many similar fields, that sometimes I think the info is another property.

When I have time to study the structure, I will see if there are alternatives that could be used in order of preference if some are missing.

I scanned a random selection of items from my pantry including supermarket own brands etc and it did find all of the ones I tried. Based on my researching yesterday this is pretty good for a free API.

I was thinking it would better to cache the returned results locally, and then next time you scan no online lookup is needed, reducing traffic to the site. (Just in case use of your flow goes viral :rofl:)

NOTE: It is open source, so if we fix up the product details as we find them, it will benefit everyone :grinning:
https://world.openfoodfacts.org/contribute

1 Like

While I was testing the changes I had implemented this morning I scanned a number of spice jars in my spice rack. This one didn't return a quantity - although all the other fields were populated.



I can even scan it using BARRED from the screen on my monitor - how cool is that?
In the object that is returned from the API call is a URL for the image...

Now all we need is for Marcus to include an 'image' field as an attribute type (ha, ha).

2 Likes

If you search the returned json you can see "en:quantity-to-be-completed"