commit 2cb7a0198832b01a228323c617af9dccba853043 Author: Navlost Date: Mon Dec 9 23:19:30 2019 +0100 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..8b28b47 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# MQTT bridge for the KDE System Monitor (ksysguard) + +A short Python script for monitoring arbitrary MQTT topics via [ksysguard](https://userbase.kde.org/KSysGuard). + +# Install + +Save the script somewhere sensible in your computer and give it executable permissions. Root is not required. + +# Configure + +Take a look the [example configuration file](./example-config.yaml) and use it as the basis for your own configuration. + +# Use + +Open ksysguard and go to `File` → `Monitor Remote Machine…`. In the dialog box, enter an arbitrary name under “Host”, select “custom command” and in the command edit box enter the full path to the script in your computer, followed by `-c` (or `--config`) and the full path to your configuration file. For instance: + +``` +/home/navlost/bin/ksysguard-sensor-mqtt.py -c /home/navlost/.config/ksysguard-sensor-mqtt.yaml +``` + +![](./doc/connect-host.png) + +Ensure that the sensor browser on the right-hand side of the ksysguard client is visible, if not, slide it into view by clicking near (but not on) the right-hand edge of the window and dragging left. You should now see your configured sensors and be able to drag them into a tab. + +![](./doc/ksysguard-window.png) diff --git a/doc/connect-host.png b/doc/connect-host.png new file mode 100644 index 0000000..08aee75 Binary files /dev/null and b/doc/connect-host.png differ diff --git a/doc/ksysguard-window.png b/doc/ksysguard-window.png new file mode 100644 index 0000000..c265067 Binary files /dev/null and b/doc/ksysguard-window.png differ diff --git a/example-config.yaml b/example-config.yaml new file mode 100644 index 0000000..8ff5de7 --- /dev/null +++ b/example-config.yaml @@ -0,0 +1,22 @@ +--- +mqtt: + host: mqtt.example.com + username: yourusername # Optional + password: yourpassword # Optional + subscriptions: + - + topic: "/things/living_room/sensors/temperature/01" + monitor: "Living room/temperature" + type: float + description: + name: "Living room temperature" + units: "° C" + - + topic: "/things/living_room/sensors/humidity/01" + monitor: "Living room/humidity" + type: integer + description: + name: "Living room relative humidity" + min: 0 + max: 100 + units: "%" diff --git a/ksysguard-sensor-mqtt.py b/ksysguard-sensor-mqtt.py new file mode 100755 index 0000000..c15bf1f --- /dev/null +++ b/ksysguard-sensor-mqtt.py @@ -0,0 +1,148 @@ +#!/usr/bin/python3 + +import os +import sys +import yaml +import paho.mqtt.client as mqtt +from random import random + +global config + +config_file_path = os.path.dirname(os.path.abspath(__file__))+"/ksysguard-sensor-mqtt.yaml" + + +# Get and parse command line arguments + +if __name__ == '__main__': + import argparse + + ap = argparse.ArgumentParser() + ap.add_argument("-c", "--config", required=False, default=None, help="path to the configuration file") + args = vars(ap.parse_args()) + + if args["config"]: + config_file_path = args["config"] + + +# Read configuration + +with open(config_file_path, "r") as config_file: + try: + config = yaml.safe_load(config_file) + except yaml.YAMLError as err: + print("Could not load configuration file") + print(err) + + +# We cache the values received via MQTT, so that they are available whenever +# requested by the ksysguard client. + +global values +values = dict() + +for subscription in config['mqtt']['subscriptions']: + values[subscription['topic']] = None + + +# Given a monitor name, returns its data if the name matches the list +# of monitors in the configuration. If not, returns None. + +def have_monitor(monitor): + return next(iter([subscription for subscription in config['mqtt']['subscriptions'] if subscription['monitor'] == monitor]), None) + + +# Given a monitor name, returns its most recent value. If the monitor +# does not exist, returns UNKNOWN COMMAND. + +def monitor_value(monitor): + subscription = have_monitor(monitor) + if subscription: + return values[subscription['topic']] + else: + return "UNKNOWN COMMAND" + +# Given a monitor name, returns its info string. If the monitor does +# not exist, returns UNKNOWN COMMAND. + +def monitor_info(monitor): + subscription = have_monitor(monitor) + #print("# Monitor info for "+monitor) + #print(subscription) + if subscription: + descr = subscription['description'] # Shorter to type + return (descr['name'] + "\t" + + (descr['min'] if 'min' in descr else "") + "\t" + + (descr['max'] if 'max' in descr else "") + "\t" + + descr['units']) + else: + return "UNKNOWN COMMAND" + + +# MQTT logic + +# The on_connect handler subscribes to all configured topics +# and issues the ksysguard prompt when finished. From that point, +# the client can start requesting data (although none may be available +# depending on the relative refresh rates of MQTT publishers and the +# ksysguard client. + +def on_connect(client, userdata, flags, rc): + print("# Connected with result code "+str(rc)) + for subscription in config['mqtt']['subscriptions']: + print("# Subscribing to " + subscription['topic']) + client.subscribe(subscription['topic']) + print("ksysguardd> ", end="", flush=True) + + +# The callback for when a PUBLISH message is received from the server. +# It stores the received value under the relevant topic key in the +# values dictionary, from where it will be retrieved when requested +# by the ksysguard client. + +def on_message(client, userdata, msg): + global values + values[msg.topic] = msg.payload.decode("utf8").strip() + + +# Create the MQTT client and assign event handlers + +client = mqtt.Client(client_id="ksysguardd-"+str(random())[2:]) +client.on_connect = on_connect +client.on_message = on_message + +if 'username' in config['mqtt'] and 'password' in config['mqtt']: + client.username_pw_set(config['mqtt']['username'], config['mqtt']['password']) +client.connect_async(config['mqtt']['host'], config['mqtt']['port'] if 'port' in config['mqtt'] else 1883, 60) + + +# Ksysguard logic + +# Start by introducing ourselves. The protocol has been mostly deduced +# from this link https://techbase.kde.org/Development/Tutorials/Sensors +# and from the ksysguardd source code. + +print("ksysguardd 4") + +client.loop_start() # Asynchronously start the MQTT client + +# Process lines of input from the ksysguard client + +for line in sys.stdin: + line = line.strip() + + if line == "monitors": # List available sensors + for subscription in config['mqtt']['subscriptions']: + print(subscription['monitor']+"\t"+subscription['type']) + + elif line == "quit": # Shut down this process + print("# Quitting") + client.loop_stop() + exit() + + elif len(line) and line[-1] == "?": # This is a query for the monitor's information + print(monitor_info(line[0:-1])) + else: # Assume that this is a query for the monitor's value + print("NaN" if monitor_value(line) == None else monitor_value(line)) + + print("ksysguardd> ", end="", flush=True) +