Monitor if laptop is on mains power or battery

Here is a little project I've been meaning to do for ages.

It produces an output whenever the laptop power changes between mains power and battery power.

Note that your Linux laptop will need sudo apt install libdbus-1-dev

Obviously, you could send the output (as I will be doing) to MQTT.

I will also have a Telegram notification as well.


[{"id":"1a63294b667c7887","type":"group","z":"956152e4.d7617","name":"Use Linux DBus to monitor mains/battery status -- auto starts \\n ","style":{"fill":"#ffffbf","fill-opacity":"0.14","label":true,"color":"#000000"},"nodes":["2b1e4c89b58d77a4","c5e845b3acb1f82a","53db23eec4d49e3c","37be3c034e5774c6"],"x":334,"y":4623,"w":542,"h":138,"info":"* Requires Node-RED 3.0+ with\r\n  functionExternalModules enabled\r\n\r\n* Auto installs dbus-next as dbus\r\n\r\n* Only works on Linux,\r\n\r\n* Requires: sudo apt install libdbus-1-dev"},{"id":"2b1e4c89b58d77a4","type":"inject","z":"956152e4.d7617","g":"1a63294b667c7887","name":"Listen","props":[],"repeat":"","crontab":"","once":true,"onceDelay":"5","topic":"","x":470,"y":4680,"wires":[["c5e845b3acb1f82a"]]},{"id":"c5e845b3acb1f82a","type":"function","z":"956152e4.d7617","g":"1a63294b667c7887","name":"Listen to D-Bus UPower","func":"// Requires Node-RED 3.0+ with functionExternalModules enabled\n//   auto installs dbus-next as dbus\n// Only works on Linux,\n//   requires: sudo apt install libdbus-1-dev\n\nlet { props, listener } = global.get('dbus-power-monitor') ?? {props: null, listener: null}\n\nnode.warn(`Props?: ${props !== null}, ${typeof props}`)\nnode.warn(`Listener?: ${listener !== null}, ${typeof listener}`)\nnode.warn(msg)\n\nif (msg.cancel) {\n    if (props && listener) {\n        node.warn('Stopping DBus listener')\n        props.off('PropertiesChanged', listener)\n        global.set('dbus-power-monitor', undefined)\n        node.status({ fill: 'grey', shape: 'ring', text: 'listener removed' })\n        node.send({\n            payload: {\n                state: null,\n                listening: false,\n                timestamp: new Date().toISOString(),\n            }\n        })\n    } else {\n        node.status({ fill: 'red', shape: 'dot', text: 'no listener to remove' })\n    }\n    return null\n}\n\n// Only set up listener once\nif (!props) {\n    node.warn('Starting DBus listener')\n    ;(async () => {\n        try {\n            const bus = dbus.systemBus()\n            const obj = await bus.getProxyObject(\n                'org.freedesktop.UPower',\n                '/org/freedesktop/UPower'\n            )\n            props = obj.getInterface('org.freedesktop.DBus.Properties')\n            global.set('dbus-power-monitor.props', props)\n\n            let lastState = null\n\n            async function publishState() {\n                const onBattery = await props.Get('org.freedesktop.UPower', 'OnBattery')\n                const state = onBattery.value ? 'BATT' : 'AC'\n                if (state !== lastState) {\n                    lastState = state\n                    node.send({\n                        payload: {\n                            state,\n                            listening: true,\n                            timestamp: new Date().toISOString()\n                        }\n                    })\n                }\n            }\n\n            // Publish initial state\n            await publishState()\n\n            // Define listener function and save reference\n            listener = async (iface, changed) => {\n                if (changed.OnBattery !== undefined) {\n                    await publishState()\n                }\n            };\n            props.on('PropertiesChanged', listener)\n            global.set('dbus-power-monitor.listener', listener)\n\n            node.status({ fill: 'green', shape: 'dot', text: 'listening' });\n\n        } catch (err) {\n            node.error('Error setting up D-Bus listener: ' + err)\n            node.status({ fill: 'red', shape: 'dot', text: 'error' })\n        }\n    })()\n}\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[{"var":"dbus","module":"dbus-next"}],"x":670,"y":4700,"wires":[["53db23eec4d49e3c"]]},{"id":"53db23eec4d49e3c","type":"debug","z":"956152e4.d7617","g":"1a63294b667c7887","name":"Power State Debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":815,"y":4700,"wires":[],"l":false},{"id":"37be3c034e5774c6","type":"inject","z":"956152e4.d7617","g":"1a63294b667c7887","name":"cancel listener","props":[{"p":"cancel","v":"true","vt":"bool"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":450,"y":4720,"wires":[["c5e845b3acb1f82a"]]}]
4 Likes

And a slightly improved version that includes MQTT-ready outputs

[{"id":"1a63294b667c7887","type":"group","z":"956152e4.d7617","name":"Use Linux DBus to monitor mains/battery status -- auto starts \\n ","style":{"fill":"#ffffbf","fill-opacity":"0.14","label":true,"color":"#000000"},"nodes":["2b1e4c89b58d77a4","c5e845b3acb1f82a","53db23eec4d49e3c","37be3c034e5774c6","98e924f27d5993d8"],"x":334,"y":4623,"w":542,"h":138,"info":"* Requires Node-RED 3.0+ with\r\n  functionExternalModules enabled\r\n\r\n* Auto installs dbus-next as dbus\r\n\r\n* Only works on Linux,\r\n\r\n* Requires: sudo apt install libdbus-1-dev"},{"id":"2b1e4c89b58d77a4","type":"inject","z":"956152e4.d7617","g":"1a63294b667c7887","name":"Listen","props":[],"repeat":"","crontab":"","once":true,"onceDelay":"5","topic":"","x":470,"y":4680,"wires":[["c5e845b3acb1f82a"]]},{"id":"c5e845b3acb1f82a","type":"function","z":"956152e4.d7617","g":"1a63294b667c7887","name":"Listen to D-Bus UPower","func":"// Requires Node-RED 3.0+ with functionExternalModules enabled\n//   auto installs dbus-next as dbus\n// Only works on Linux,\n//   requires: sudo apt install libdbus-1-dev\n\nconst mqttTopic = 'INFO/system/power'\n\nlet { props, listener } = global.get('dbus-power-monitor') ?? {props: null, listener: null}\n\nif (msg.cancel) {\n    if (props && listener) {\n        node.warn('Stopping DBus listener')\n        props.off('PropertiesChanged', listener)\n        global.set('dbus-power-monitor', undefined)\n        node.status({ fill: 'grey', shape: 'ring', text: 'listener removed' })\n        node.send({\n            payload: {\n                state: null,\n                listening: false,\n                timestamp: new Date().toISOString(),\n            }\n        })\n    } else {\n        node.status({ fill: 'red', shape: 'dot', text: 'no listener to remove' })\n    }\n    return null\n}\n\n// Only set up listener once\nif (!props) {\n    node.warn('Starting DBus listener')\n    ;(async () => {\n        try {\n            const bus = dbus.systemBus()\n            const obj = await bus.getProxyObject(\n                'org.freedesktop.UPower',\n                '/org/freedesktop/UPower'\n            )\n            props = obj.getInterface('org.freedesktop.DBus.Properties')\n            global.set('dbus-power-monitor.props', props)\n\n            let lastState = null\n\n            async function publishState() {\n                const onBattery = await props.Get('org.freedesktop.UPower', 'OnBattery')\n                const state = onBattery.value ? 'BATT' : 'AC'\n                const timestamp = new Date().toISOString()\n                if (state !== lastState) {\n                    lastState = state\n                    node.send([\n                        {\n                            payload: {\n                                state,\n                                listening: true,\n                                timestamp: timestamp,\n                            }\n                        },\n                        [\n                            {\n                                topic: mqttTopic,\n                                payload: state,\n                            },\n                            {\n                                topic: `${mqttTopic}/timestamp`,\n                                payload: timestamp,\n                            },\n                        ],\n                    ])\n                }\n            }\n\n            // Publish initial state\n            await publishState()\n\n            // Define listener function and save reference\n            listener = async (iface, changed) => {\n                if (changed.OnBattery !== undefined) {\n                    await publishState()\n                }\n            };\n            props.on('PropertiesChanged', listener)\n            global.set('dbus-power-monitor.listener', listener)\n\n            node.status({ fill: 'green', shape: 'dot', text: 'listening' });\n\n        } catch (err) {\n            node.error('Error setting up D-Bus listener: ' + err)\n            node.status({ fill: 'red', shape: 'dot', text: 'error' })\n        }\n    })()\n}\n","outputs":2,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[{"var":"dbus","module":"dbus-next"}],"x":670,"y":4700,"wires":[["53db23eec4d49e3c"],["98e924f27d5993d8"]],"outputLabels":["status","mqtt"]},{"id":"53db23eec4d49e3c","type":"debug","z":"956152e4.d7617","g":"1a63294b667c7887","name":"Power State Debug","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":815,"y":4680,"wires":[],"l":false},{"id":"37be3c034e5774c6","type":"inject","z":"956152e4.d7617","g":"1a63294b667c7887","name":"cancel listener","props":[{"p":"cancel","v":"true","vt":"bool"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":450,"y":4720,"wires":[["c5e845b3acb1f82a"]]},{"id":"98e924f27d5993d8","type":"link out","z":"956152e4.d7617","g":"1a63294b667c7887","name":"docker-compose ->","mode":"link","links":["4dee6cef8009666a"],"x":815,"y":4720,"wires":[]}]
1 Like