Needs: cheerio npm module (in node-red addon config), and the HACS node-red companion (then install the integration).
Last active
January 21, 2026 09:03
-
-
Save remy/23da73214ea5035d56377bd4f14fd5c2 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // just the javascript parse function so you can understand what's happening: | |
| import * as cheerio from 'cheerio'; | |
| const res = await fetch('https://www.buses.co.uk/stops/149000006328', { | |
| headers: { | |
| 'X-Requested-With': 'XMLHttpRequest', | |
| }, | |
| }); | |
| const msg = { | |
| payload: await res.text(), | |
| }; | |
| console.log(main(msg)); | |
| function main(msg) { | |
| const html = msg.payload; | |
| const $ = cheerio.load(html); | |
| msg.payload = getDate($); | |
| return msg; | |
| function getDate($) { | |
| return $('.departure-board li') | |
| .map((i, _) => { | |
| const service = $('.single-visit__name', _).text(); | |
| const destination = $('.single-visit__description', _).text(); | |
| const live = $('.sr-only', _).text().includes('Live.'); | |
| const time = $('.single-visit__arrival-time__cell', _).text(); | |
| let datetime; | |
| if (time === 'Due') { | |
| datetime = new Date(); | |
| } else if (live) { | |
| datetime = new Date(Date.now() + parseInt(time, 10) * 1000 * 60); | |
| } else { | |
| datetime = new Date(); | |
| const [hour, min] = time.split(':').map((_) => parseInt(_, 10)); | |
| const currHour = new Date().getHours(); | |
| if (hour < currHour) { | |
| // then it's tomorrow | |
| datetime.setTime(Date.now() + 24 * 60 * 60 * 1000); | |
| } | |
| datetime.setHours(hour); | |
| datetime.setMinutes(min); | |
| } | |
| const res = { | |
| service, | |
| destination, | |
| time: datetime.toISOString(), | |
| live, | |
| }; | |
| return res; | |
| }) | |
| .get(); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [ | |
| { | |
| "id": "38b8212ab1435bec", | |
| "type": "tab", | |
| "label": "Next bus times", | |
| "disabled": false, | |
| "info": "Requires two additional parts to work:\n\n 1. cheerio added to the node-red configuration in the npm packages\n 2. node-red HACS companion - to be able to set the bus times", | |
| "env": [] | |
| }, | |
| { | |
| "id": "3a1b584daa98ed34", | |
| "type": "http request", | |
| "z": "38b8212ab1435bec", | |
| "name": "", | |
| "method": "GET", | |
| "ret": "txt", | |
| "paytoqs": "ignore", | |
| "url": "", | |
| "tls": "", | |
| "persist": false, | |
| "proxy": "", | |
| "insecureHTTPParser": false, | |
| "authType": "", | |
| "senderr": false, | |
| "headers": [ | |
| { | |
| "keyType": "other", | |
| "keyValue": "X-Requested-With", | |
| "valueType": "other", | |
| "valueValue": "XMLHttpRequest" | |
| } | |
| ], | |
| "x": 410, | |
| "y": 200, | |
| "wires": [ | |
| [ | |
| "4a2d55de365bcf89", | |
| "367a621d38fb9f80" | |
| ] | |
| ] | |
| }, | |
| { | |
| "id": "1faaae10b0b18097", | |
| "type": "inject", | |
| "z": "38b8212ab1435bec", | |
| "name": "set bus id on deploy", | |
| "props": [ | |
| { | |
| "p": "payload" | |
| } | |
| ], | |
| "repeat": "", | |
| "crontab": "", | |
| "once": false, | |
| "onceDelay": 0.1, | |
| "topic": "", | |
| "payload": "149000006328", | |
| "payloadType": "str", | |
| "x": 150, | |
| "y": 100, | |
| "wires": [ | |
| [ | |
| "fd5c357dd630bd2b" | |
| ] | |
| ] | |
| }, | |
| { | |
| "id": "4a2d55de365bcf89", | |
| "type": "function", | |
| "z": "38b8212ab1435bec", | |
| "name": "parse", | |
| "func": "const html = msg.payload;\n\nconst $ = cheerio.load(html);\n\nconst res = getDate($);\n\nreturn {\n payload: res[0].due,\n schedule: res,\n};\n\nfunction getDate($) {\n return $('.departure-board li')\n .map((i, _) => {\n const route = $('.single-visit__name', _).text();\n const destination = $('.single-visit__description', _).text();\n const live = $('.sr-only', _).text().includes('Live.');\n let estimated = $('.single-visit__arrival-time__cell', _).text();\n\n let datetime;\n\n if (estimated === 'Due') {\n datetime = new Date();\n estimated = 0;\n } else if (live) {\n datetime = new Date(Date.now() + parseInt(estimated, 10) * 1000 * 60);\n estimated = parseInt(estimated, 10);\n } else {\n datetime = new Date();\n const [hour, min] = estimated.split(':').map((_) => parseInt(_, 10));\n const currHour = new Date().getHours();\n\n if (hour < currHour) {\n // then it's tomorrow\n datetime.setTime(Date.now() + 24 * 60 * 60 * 1000);\n }\n datetime.setHours(hour);\n datetime.setMinutes(min);\n estimated = null;\n }\n\n const res = {\n route,\n destination,\n due: datetime.toJSON(),\n estimated,\n live,\n };\n\n return res;\n })\n .get();\n}", | |
| "outputs": 1, | |
| "timeout": 0, | |
| "noerr": 0, | |
| "initialize": "", | |
| "finalize": "", | |
| "libs": [ | |
| { | |
| "var": "cheerio", | |
| "module": "cheerio" | |
| } | |
| ], | |
| "x": 270, | |
| "y": 280, | |
| "wires": [ | |
| [ | |
| "6a39627fdba30e30", | |
| "8589ee006dccfb5b" | |
| ] | |
| ] | |
| }, | |
| { | |
| "id": "6a39627fdba30e30", | |
| "type": "debug", | |
| "z": "38b8212ab1435bec", | |
| "name": "debug 2", | |
| "active": false, | |
| "tosidebar": true, | |
| "console": true, | |
| "tostatus": false, | |
| "complete": "true", | |
| "targetType": "full", | |
| "statusVal": "", | |
| "statusType": "auto", | |
| "x": 410, | |
| "y": 360, | |
| "wires": [] | |
| }, | |
| { | |
| "id": "8589ee006dccfb5b", | |
| "type": "ha-sensor", | |
| "z": "38b8212ab1435bec", | |
| "name": "Next Bus", | |
| "entityConfig": "f1181ce963e4b8c2", | |
| "version": 0, | |
| "state": "payload", | |
| "stateType": "msg", | |
| "attributes": [ | |
| { | |
| "property": "schedule", | |
| "value": "$.schedule", | |
| "valueType": "jsonata" | |
| } | |
| ], | |
| "inputOverride": "allow", | |
| "outputProperties": [], | |
| "x": 400, | |
| "y": 280, | |
| "wires": [ | |
| [] | |
| ] | |
| }, | |
| { | |
| "id": "c5548cd78686562c", | |
| "type": "cronplus", | |
| "z": "38b8212ab1435bec", | |
| "name": "", | |
| "outputField": "payload", | |
| "timeZone": "", | |
| "storeName": "", | |
| "commandResponseMsgOutput": "output1", | |
| "defaultLocation": "", | |
| "defaultLocationType": "default", | |
| "outputs": 1, | |
| "options": [ | |
| { | |
| "name": "schedule", | |
| "topic": "url", | |
| "payloadType": "str", | |
| "payload": "https://www.buses.co.uk/stops/149000006328", | |
| "expressionType": "cron", | |
| "expression": "* 6-23 * * *", | |
| "location": "", | |
| "offset": "0", | |
| "solarType": "all", | |
| "solarEvents": "sunrise,sunset" | |
| } | |
| ], | |
| "x": 120, | |
| "y": 200, | |
| "wires": [ | |
| [ | |
| "994818c5617db720" | |
| ] | |
| ] | |
| }, | |
| { | |
| "id": "367a621d38fb9f80", | |
| "type": "debug", | |
| "z": "38b8212ab1435bec", | |
| "name": "debug 1", | |
| "active": false, | |
| "tosidebar": true, | |
| "console": true, | |
| "tostatus": false, | |
| "complete": "true", | |
| "targetType": "full", | |
| "statusVal": "", | |
| "statusType": "auto", | |
| "x": 610, | |
| "y": 200, | |
| "wires": [] | |
| }, | |
| { | |
| "id": "994818c5617db720", | |
| "type": "function", | |
| "z": "38b8212ab1435bec", | |
| "name": "get bus id", | |
| "func": "const stopId = context.get(\"busStopId\") || \"149000006328\";\nmsg.url = `https://www.buses.co.uk/stops/${stopId}`;\nreturn msg;", | |
| "outputs": 1, | |
| "timeout": 0, | |
| "noerr": 0, | |
| "initialize": "", | |
| "finalize": "", | |
| "libs": [], | |
| "x": 260, | |
| "y": 200, | |
| "wires": [ | |
| [ | |
| "3a1b584daa98ed34" | |
| ] | |
| ] | |
| }, | |
| { | |
| "id": "fd5c357dd630bd2b", | |
| "type": "function", | |
| "z": "38b8212ab1435bec", | |
| "name": "save bus id", | |
| "func": "context.set(\"busStopId\", msg.payload);\nreturn msg;", | |
| "outputs": 1, | |
| "timeout": 0, | |
| "noerr": 0, | |
| "initialize": "", | |
| "finalize": "", | |
| "libs": [], | |
| "x": 390, | |
| "y": 100, | |
| "wires": [ | |
| [] | |
| ] | |
| }, | |
| { | |
| "id": "f1181ce963e4b8c2", | |
| "type": "ha-entity-config", | |
| "server": "3cacbe68.577112", | |
| "deviceConfig": "", | |
| "name": "next_bus", | |
| "version": 6, | |
| "entityType": "sensor", | |
| "haConfig": [ | |
| { | |
| "property": "name", | |
| "value": "Next Bus" | |
| }, | |
| { | |
| "property": "icon", | |
| "value": "mdi:bus" | |
| }, | |
| { | |
| "property": "entity_picture", | |
| "value": "" | |
| }, | |
| { | |
| "property": "entity_category", | |
| "value": "" | |
| }, | |
| { | |
| "property": "device_class", | |
| "value": "timestamp" | |
| }, | |
| { | |
| "property": "unit_of_measurement", | |
| "value": "" | |
| }, | |
| { | |
| "property": "state_class", | |
| "value": "" | |
| } | |
| ], | |
| "resend": false, | |
| "debugEnabled": true | |
| }, | |
| { | |
| "id": "3cacbe68.577112", | |
| "type": "server", | |
| "name": "Home Assistant", | |
| "addon": true, | |
| "rejectUnauthorizedCerts": true, | |
| "ha_boolean": [], | |
| "connectionDelay": false, | |
| "cacheJson": false, | |
| "heartbeat": true, | |
| "heartbeatInterval": "10", | |
| "statusSeparator": "", | |
| "enableGlobalContextStore": false | |
| }, | |
| { | |
| "id": "795661d7d798f7ec", | |
| "type": "global-config", | |
| "env": [], | |
| "modules": { | |
| "node-red-contrib-home-assistant-websocket": "0.80.3", | |
| "node-red-contrib-cron-plus": "2.2.4" | |
| } | |
| } | |
| ] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment