Now that RV-C is working properly, it would be great if all of the RV-C devices could be discovered and written to in Node Red. It would be way cool to use the Cerbo as a gateway and build some panels to interact with 3rd party devices.
For anyone that runs across this, itâs essentially already implemented and works well. You just need to install the socketcan palette, which will be set to communicate on can0. Here is an example flow that will switch an output on a Firefly G12 and read the status back. Just be careful to filter your DGNâs so you arenât creating a giant cpu load. If you do overload the cpu, node-red will stop responding. You can put node red into safe mode to resolve it. You will need the flowfuse dashboard for the button group.
[
{
"id": "924a78e23a6b67c1",
"type": "tab",
"label": "Flow 2",
"disabled": false,
"info": "",
"env": []
},
{
"id": "05c780adab08450a",
"type": "socketcan-in",
"z": "924a78e23a6b67c1",
"name": "socketcan-in",
"config": "6ecb6c96f6214bc4",
"x": 870,
"y": 60,
"wires": []
},
{
"id": "03a88c8b3a09d176",
"type": "socketcan-out",
"z": "924a78e23a6b67c1",
"name": "socketcan-out",
"config": "6ecb6c96f6214bc4",
"x": 130,
"y": 60,
"wires": [
[
"0240f6de4bc36578"
]
]
},
{
"id": "5b6617a157262e0f",
"type": "function",
"z": "924a78e23a6b67c1",
"name": "Encode to G12",
"func": "// Generic function to send on/off to G12\n// Send DIMMER_COMMAND_2 (0x1FEDB)\n// Use true/false boolean input\n\nconst DGN = 0x1FEDB; // DGN for G12\nconst INSTANCE = 15; // set to G12 Output\nconst DEST_ADDR = 0x01; // adjust if you need a specific target SA\n\nfunction buildFrame(dgn, da, data) {\n return {\n canid: (dgn << 8) | (da & 0xFF),\n ext: true,\n rtr: false,\n dlc: data.length,\n data\n };\n}\n\n// Normalize incoming payload to a 0..251 \"desired level\"\nfunction toLevel(input) {\n let v = input;\n\n if (v && typeof v === \"object\") {\n if (\"desiredLevel\" in v) v = v.desiredLevel;\n else if (\"level\" in v) v = v.level;\n else if (\"on\" in v) return v.on ? 251 : 0;\n }\n\n if (typeof v === \"string\") {\n const s = v.trim().toLowerCase();\n if (s === \"on\" || s === \"true\") return 251;\n if (s === \"off\"|| s === \"false\") return 0;\n const n = Number(s);\n if (!Number.isNaN(n)) v = n;\n }\n\n if (typeof v === \"boolean\") return v ? 251 : 0;\n if (typeof v !== \"number\" || Number.isNaN(v)) return 0;\n\n // Treat 0..1 as fraction, 0..100 as percent, otherwise raw 0..251\n if (v > 0 && v <= 1) return Math.max(0, Math.min(251, Math.round(v * 251)));\n if (v >= 0 && v <= 100) return Math.max(0, Math.min(251, Math.round(v * 2.51)));\n return Math.max(0, Math.min(251, Math.round(v)));\n}\n\nlet group = 0;\nif (msg.payload && typeof msg.payload === \"object\" && \"group\" in msg.payload) {\n const g = Number(msg.payload.group);\n if (!Number.isNaN(g)) group = g & 0xFF;\n}\n\nconst level = toLevel(msg.payload);\n\n// Build and send frame: [instance, group, level]\nconst frame = buildFrame(DGN, DEST_ADDR, [INSTANCE, group, level]);\n\nreturn { payload: frame };\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 680,
"y": 60,
"wires": [
[
"05c780adab08450a"
]
]
},
{
"id": "9e3502d99bf9b11d",
"type": "ui-button-group",
"z": "924a78e23a6b67c1",
"name": "",
"group": "3df0d3490710d5a7",
"order": 4,
"width": "0",
"height": "0",
"label": "G12 Switch",
"className": "",
"rounded": true,
"useThemeColors": false,
"passthru": false,
"options": [
{
"label": "Off",
"icon": "",
"value": "false",
"valueType": "bool",
"color": "#d4d4d4"
},
{
"label": "On",
"icon": "",
"value": "true",
"valueType": "bool",
"color": "#f4bc25"
}
],
"topic": "topic",
"topicType": "msg",
"x": 490,
"y": 60,
"wires": [
[
"5b6617a157262e0f"
]
]
},
{
"id": "0240f6de4bc36578",
"type": "function",
"z": "924a78e23a6b67c1",
"name": "Decode G12",
"func": "// Generic DIMMER status â ui_switch (boolean)\n// Accepts PGNs: 0x1FEDB (command) and 0x1FEDA (status)\n// Set the instance to watch here:\nconst INSTANCE = 15; // â change thisto the G12 output. (set to -1 to disable instance filtering)\n\nlet frame = msg.payload;\nif (!frame || typeof frame.canid !== \"number\" || !Array.isArray(frame.data)) return null;\n\nconst dgn = (frame.canid >>> 8) & 0x3FFFF;\nif (dgn !== 0x1FEDB && dgn !== 0x1FEDA) return null;\n\nconst instance = Number(frame.data[0] ?? -1);\nif (INSTANCE >= 0 && instance !== INSTANCE) return null;\n\n// Level at byte 2 (0..251). Any nonzero = ON\nconst level = Number(frame.data[2] ?? 0);\nconst on = level > 0;\n\n// Output to drive a ui_switch\nmsg.payload = on; // true/false\nmsg.topic = `device/${instance}/status`;\nmsg.meta = {\n dgn_hex: \"0x\" + dgn.toString(16).toUpperCase(),\n instance,\n level,\n sa: frame.canid & 0xFF\n};\n\nreturn msg;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 310,
"y": 60,
"wires": [
[
"9e3502d99bf9b11d"
]
]
},
{
"id": "6ecb6c96f6214bc4",
"type": "socketcan-config",
"interface": "can0"
},
{
"id": "3df0d3490710d5a7",
"type": "ui-group",
"name": "Switches",
"page": "30186ff8bd45c991",
"width": "2",
"height": "1",
"order": 2,
"showTitle": true,
"className": "",
"visible": "true",
"disabled": "false",
"groupType": "default"
},
{
"id": "30186ff8bd45c991",
"type": "ui-page",
"name": "Page 1",
"ui": "d3ede36e308c0fa4",
"path": "/page1",
"icon": "home",
"layout": "grid",
"theme": "cc0aada03716a7fe",
"breakpoints": [
{
"name": "Default",
"px": "0",
"cols": "3"
},
{
"name": "Tablet",
"px": "576",
"cols": "6"
},
{
"name": "Small Desktop",
"px": "768",
"cols": "9"
},
{
"name": "Desktop",
"px": "1024",
"cols": "12"
}
],
"order": 1,
"className": "",
"visible": "true",
"disabled": "false"
},
{
"id": "d3ede36e308c0fa4",
"type": "ui-base",
"name": "My Dashboard",
"path": "/dashboard",
"appIcon": "",
"includeClientData": true,
"acceptsClientConfig": [
"ui-notification",
"ui-control"
],
"showPathInSidebar": false,
"headerContent": "page",
"navigationStyle": "none",
"titleBarStyle": "hidden",
"showReconnectNotification": true,
"notificationDisplayTime": 1,
"showDisconnectNotification": true,
"allowInstall": false
},
{
"id": "cc0aada03716a7fe",
"type": "ui-theme",
"name": "Default Theme",
"colors": {
"surface": "#ffffff",
"primary": "#0094ce",
"bgPage": "#eeeeee",
"groupBg": "#ffffff",
"groupOutline": "#cccccc"
},
"sizes": {
"density": "comfortable",
"pagePadding": "12px",
"groupGap": "12px",
"groupBorderRadius": "4px",
"widgetGap": "12px"
}
}
]
1 Like
