From fd00494c8e939bead4e3b9985bf65b2910f8a801 Mon Sep 17 00:00:00 2001 From: Daniel Napora Date: Sat, 6 Feb 2021 14:56:03 +0100 Subject: [PATCH] optional mini HW monitor in tray - phwmon.py --- bin/compton_toggle | 2 +- bin/mabox-obstart | 40 ++++- bin/phwmon.py | 373 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 413 insertions(+), 2 deletions(-) create mode 100755 bin/phwmon.py diff --git a/bin/compton_toggle b/bin/compton_toggle index 4197e17..78cfd82 100755 --- a/bin/compton_toggle +++ b/bin/compton_toggle @@ -1,6 +1,6 @@ #!/bin/bash -if [ `pidof picom` ]; then +if [ $(pidof picom) ]; then echo "Stopping picom" killall picom else diff --git a/bin/mabox-obstart b/bin/mabox-obstart index c8d8ef6..fb1e021 100755 --- a/bin/mabox-obstart +++ b/bin/mabox-obstart @@ -33,6 +33,27 @@ VBoxManage list -s vms | cut -f 2 -d "\"" | sort -f | while read vm printf "%s\n%s\n" "^sep()" "$REFRESH,/usr/bin/mabox-obstart virtualboxes" >>$HOME/.config/mabox/vboxes.csv fi } + +phwmon() { +#kill phwmon.py if running +if phwmonpid=$(pgrep -f phwmon.py); then +kill $phwmonpid +fi +. $HOME/.config/mabox/mabox.conf +if [ $phwmon_monitor == true ];then + +[[ $phwmon_cpu == true ]] && cpu="--cpu" || cpu="" +[[ $phwmon_mem == true ]] && mem="--mem" || mem="" +[[ $phwmon_swap == true ]] && swap="--swap" || swap="" +[[ $phwmon_net == true ]] && net="--net" || net="" +[[ $phwmon_io == true ]] && io="--io" || io="" + +phwmon.py ${cpu} ${mem} ${swap} ${net} ${io} --task_manager lxtask + +fi + +} + startopenbox() { # Copy only new files from /etc/xdg/autostart/ config_dir=${XDG_CONFIG_HOME:-$HOME/.config} @@ -42,14 +63,31 @@ rsync -aq --ignore-existing /etc/xdg/autostart/ $config_dir/autostart # Run mwelcome if not disaled [ ! -f "$HOME/.config/mabox/.mwelcome" ] && mwelcome & +. $HOME/.config/mabox/mabox.conf +#Set config variables if not set or empty; ":" means do nothing +# NEW CONFIG VARIABLES - SET defaults at openbox start +[[ -v phwmon_monitor ]] && : || mb-setvar phwmon_monitor=false +[[ -v phwmon_cpu ]] && : || mb-setvar phwmon_cpu=true +[[ -v phwmon_mem ]] && :|| mb-setvar phwmon_mem=true +[[ -v phwmon_swap ]] && : || mb-setvar phwmon_swap=false +[[ -v phwmon_net ]] && : || mb-setvar phwmon_net=true +[[ -v phwmon_io ]] && : || mb-setvar phwmon_io=false +[[ -v places_tint2pipe ]] && : || mb-setvar places_tint2pipe=true +[[ -v places_quicknav ]] && : || mb-setvar places_quicknav=true +[[ -v places_bookmarks ]] && : || mb-setvar places_bookmarks=true + virtualboxes +if command -v phwmon.py &>/dev/null; then +phwmon +fi } case "$1" in startopenbox) startopenbox;; virtualboxes) virtualboxes;; + phwmon) phwmon;; *) -echo -e "Usage $(basename "$0") startopenbox|virtualboxes" >&2 +echo -e "Usage $(basename "$0") startopenbox|virtualboxes|phwmon" >&2 exit 1 ;; esac diff --git a/bin/phwmon.py b/bin/phwmon.py new file mode 100755 index 0000000..c9df17f --- /dev/null +++ b/bin/phwmon.py @@ -0,0 +1,373 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GPLv2 + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, Gdk, GdkPixbuf, GLib + +import argparse +import cairo +import subprocess + +import psutil + +def normalize_color_hex(s): + if s[0] == "#": + s = s[1:] + if len(s) == 3: + s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2] + if len(s) == 4: + s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2] + s[3] + s[3] + if len(s) == 6: + s += "ff" + assert(len(s) == 8) + return s + +def color_hex_to_float(s): + s = normalize_color_hex(s) + return [int(s[0:2], 16)/255.0, int(s[2:4], 16)/255.0, int(s[4:6], 16)/255.0, int(s[6:8], 16)/255.0] + +def color_hex_to_int(s): + s = normalize_color_hex(s) + return int(s, 16) + +def bytes2human(n): + # http://code.activestate.com/recipes/578019 + # >>> bytes2human(10000) + # '9.8 K' + # >>> bytes2human(100001221) + # '95.4 M' + symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') + prefix = {} + for i, s in enumerate(symbols): + prefix[s] = 1 << (i + 1) * 10 + for s in reversed(symbols): + if n >= prefix[s]: + value = float(n) / prefix[s] + return '%.1f %sB' % (value, s) + return "%s B" % n + + +# Parameters +parser = argparse.ArgumentParser() +parser.add_argument("--cpu", help="Show a CPU activity graph", dest="cpu", action="store_true") +parser.add_argument("--cpu_alert_max", help="Blink when CPU load goes above defined percentage. Default: 100", dest="cpu_alert_max", default=100, type=int) +parser.add_argument("--core", help="Show a CPU activity graph for each logical CPU core", dest="core", action="store_true") +parser.add_argument("--mem", help="Show a memory usage graph", dest="mem", action="store_true") +parser.add_argument("--mem_percent", help="Tooltip: Show used memory in percentage", dest="mem_percent", action="store_true") +parser.add_argument("--mem_alert_max", help="Blink when memory usage goes above defined percentage. Default: 100", dest="mem_alert_max", default=100, type=int) +parser.add_argument("--swap", help="Show a swap usage graph", dest="swap", action="store_true") +parser.add_argument("--swap_percent", help="Tooltip: Show used swap in percentage", dest="swap_percent", action="store_true") +parser.add_argument("--swap_alert_max", help="Blink when swap usage goes above defined percentage. Default: 100", dest="swap_alert_max", default=100, type=int) +parser.add_argument("--net", help="Show a network usage graph", dest="net", action="store_true") +parser.add_argument("--net_scale", help="Maximum value for the network usage graph, in Mbps. Default: 40.", default=40, type=int) +parser.add_argument("--net_alert_max", help="Blink when network usage goes above defined percentage. Default: 100 ", dest="net_alert_max", default=100, type=int) +parser.add_argument("--io", help="Show a disk I/O graph", dest="io", action="store_true") +parser.add_argument("--io_scale", help="Maximum value for the disk I/O graph, in MB/s. Default: 100.", default=100, type=int) +parser.add_argument("--io_alert_max", help="Blink when disk I/O goes above defined percentage. Default: 100", dest="io_alert_max", default=100, type=int) + +parser.add_argument("--size", help="Icon size in pixels. Default: 22.", default=22, type=int) +parser.add_argument("--invert", help="Try to invert order of icons ( might not work as it depends highly on the system tray used )", dest="invert", action="store_true") +parser.add_argument("--interval", help="Refresh interval in miliseconds. Default: 1000 (1sec).", default=1000, type=int) +parser.add_argument("--alert_interval", help="Alert blink interval in miliseconds. Default: 1000 (1sec).", default=1000, type=int) +parser.add_argument("--bg", help="Background color (RGBA hex). Default: #DDDDDD60.", default="#DDDDDD60") +parser.add_argument("--fg_cpu", help="CPU graph color (RGBA hex). Default: #70b433.", default="#70b433") +parser.add_argument("--fg_mem", help="Memory graph color (RGBA hex). Default: #efc541.", default="#efc541") +parser.add_argument("--fg_swap", help="Swap graph color (RGBA hex). Default: #ff81ca.", default="#ff81ca") +parser.add_argument("--fg_net", help="Network graph color (RGBA hex). Default: #368aeb.", default="#368aeb") +parser.add_argument("--fg_io", help="Disk I/O graph color (RGBA hex). Default: #ff5e56.", default="#ff5e56") +parser.add_argument("--fg_alert", help="Alert color (RGBA hex). Default: #ff0000cc.", default="#ff0000cc") + +parser.add_argument("--task_manager", help="Task manager to execute on left click. Default: None.") + +parser.set_defaults(cpu=False) +parser.set_defaults(core=False) +parser.set_defaults(mem=False) +parser.set_defaults(swap=False) +parser.set_defaults(net=False) +parser.set_defaults(io=False) +args = parser.parse_args() + +w = h = args.size +invert = args.invert +interval = args.interval +alertInterval = args.alert_interval +bgCol = color_hex_to_int(args.bg) +fgCpu = color_hex_to_float(args.fg_cpu) +fgRam = color_hex_to_float(args.fg_mem) +fgSwap = color_hex_to_float(args.fg_swap) +fgNet = color_hex_to_float(args.fg_net) +fgDiskIo = color_hex_to_float(args.fg_io) +fgAlert = color_hex_to_int(args.fg_alert) + +cpuEnabled = args.cpu or args.core +cpuAlertMax = args.cpu_alert_max +mergeCpus = not args.core +ramEnabled = args.mem +memPercent = args.mem_percent +memAlertMax = args.mem_alert_max +swapEnabled = args.swap +swapPercent = args.swap_percent +swapAlertMax = args.swap_alert_max +netEnabled = args.net +netScale = args.net_scale +netAlertMax = args.net_alert_max +diskIoEnabled = args.io +diskIoScale = args.io_scale +diskIoAlertMax = args.io_alert_max +taskMgr = args.task_manager + +if not cpuEnabled and not ramEnabled and not swapEnabled and not netEnabled and not diskIoEnabled: + cpuEnabled = mergeCpus = ramEnabled = swapEnabled = netEnabled = diskIoEnabled = True + +class HardwareMonitor: + alertState = False + + def __init__(self): + if invert: + self.initDiskIo() + self.initNet() + self.initSwap() + self.initRam() + self.initCpus() + self.drawDiskIo() + self.drawNet() + self.drawSwap() + self.drawRam() + self.drawCpus() + else: + self.initCpus() + self.initRam() + self.initSwap() + self.initNet() + self.initDiskIo() + self.drawCpus() + self.drawRam() + self.drawSwap() + self.drawNet() + self.drawDiskIo() + GLib.timeout_add(interval, self.update) + GLib.timeout_add(alertInterval, self.alertFlip) + + def alertFlip(self): + self.alertState = not self.alertState + return True + + def rightClickEvent(self, icon, button, time): + menu = Gtk.Menu() + quit = Gtk.MenuItem("Quit") + quit.connect("activate", Gtk.main_quit) + menu.append(quit) + menu.show_all() + menu.popup(None, None, Gtk.status_icon_position_menu, button, time, icon) + + def leftClickEvent (self, icon): + if not taskMgr: + return + subprocess.Popen(taskMgr) + + def initCpus(self): + if not cpuEnabled: + return + if mergeCpus: + self.cpus = [[0 for x in range(w)]] + psutil.cpu_percent(percpu=mergeCpus) + else: + self.cpus = [[0 for x in range(w)] for c in range(len(psutil.cpu_percent(percpu=not mergeCpus)))] + self.cpuIcons = [Gtk.StatusIcon() for c in range(len(self.cpus))] + for c in range(len(self.cpus)): + self.cpuIcons[c].set_title("hwmon 1 cpu{0}".format("" if mergeCpus else (" " + str(c+1)))) + self.cpuIcons[c].connect("popup-menu", self.rightClickEvent) + self.cpuIcons[c].connect("activate", self.leftClickEvent) + self.cpuIcons[c].set_visible(True) + + def updateCpus(self): + if not cpuEnabled: + return + vals = psutil.cpu_percent(percpu=not mergeCpus) + if mergeCpus: + vals = [vals] + for c in range(len(vals)): + self.cpus[c].append(vals[c]) + self.cpus[c].pop(0) + self.cpuIcons[c].set_tooltip_text("CPU{0}: {1}%".format("" if mergeCpus else (" " + str(c+1)), vals[c])) + + def drawCpus(self): + if not cpuEnabled: + return + for c in range(len(self.cpus)): + self.drawGraph(self.cpus[c], self.cpuIcons[c], bgCol, fgCpu) + self.drawAlert(self.cpus[c], self.cpuIcons[c], fgAlert, cpuAlertMax) + + def initRam(self): + if not ramEnabled: + return + self.ram = [0 for x in range(w)] + self.ramIcon = Gtk.StatusIcon() + self.ramIcon.set_title("hwmon 2 memory") + self.ramIcon.connect("popup-menu", self.rightClickEvent) + self.ramIcon.connect("activate", self.leftClickEvent) + self.ramIcon.set_visible(True) + + def updateRam(self): + if not ramEnabled: + return + mem = psutil.virtual_memory() + total = mem[0] + used = mem[3] + used_percent = mem[2] + self.ram.append(used_percent) + self.ram.pop(0) + if memPercent: + self.ramIcon.set_tooltip_text("Memory: %d%% used of %s" % (used_percent, bytes2human(total))) + else: + self.ramIcon.set_tooltip_text("Memory: %s used of %s" % (bytes2human(used), bytes2human(total))) + + def drawRam(self): + if not ramEnabled: + return + self.drawGraph(self.ram, self.ramIcon, bgCol, fgRam) + self.drawAlert(self.ram, self.ramIcon, fgAlert, memAlertMax) + + def initSwap(self): + if not swapEnabled: + return + self.swap = [0 for x in range(w)] + self.swapIcon = Gtk.StatusIcon() + self.swapIcon.set_title("hwmon 3 swap") + self.swapIcon.connect("popup-menu", self.rightClickEvent) + self.swapIcon.connect("activate", self.leftClickEvent) + + def updateSwap(self): + if not swapEnabled: + return + swap = psutil.swap_memory() + total = swap[0] + used = swap[1] + used_percent = swap[3] + if bool(self.swapIcon.get_visible()) != bool(total): + self.swapIcon.set_visible(total) + self.swap.append(used_percent) + self.swap.pop(0) + if swapPercent: + self.swapIcon.set_tooltip_text("Swap: %d%% used of %s" % (used_percent, bytes2human(total))) + else: + self.swapIcon.set_tooltip_text("Swap: %s used of %s" % (bytes2human(used), bytes2human(total))) + + def drawSwap(self): + if not swapEnabled: + return + self.drawGraph(self.swap, self.swapIcon, bgCol, fgSwap) + self.drawAlert(self.swap, self.swapIcon, fgAlert, swapAlertMax) + + def initNet(self): + if not netEnabled: + return + self.net = [0 for x in range(w)] + v = psutil.net_io_counters(pernic=False) + self.netBytes = v[0] + v[1] + self.netIcon = Gtk.StatusIcon() + self.netIcon.set_title("hwmon 4 network") + self.netIcon.connect("popup-menu", self.rightClickEvent) + self.netIcon.connect("activate", self.leftClickEvent) + self.netIcon.set_visible(True) + + def updateNet(self): + if not netEnabled: + return + v = psutil.net_io_counters(pernic=False) + v = v[0] + v[1] + delta = v - self.netBytes + self.netBytes = v + self.net.append(delta * 8 / 1.0e6) + self.net.pop(0) + self.netIcon.set_tooltip_text("Network: %.1f Mb/s" % (delta * 8 / 1.0e6)) + + def drawNet(self): + if not netEnabled: + return + self.drawGraph(self.net, self.netIcon, bgCol, fgNet, netScale) + self.drawAlert(self.net, self.netIcon, fgAlert, netAlertMax, netScale) + + def initDiskIo(self): + if not diskIoEnabled: + return + self.diskIo = [0 for x in range(w)] + v = psutil.disk_io_counters(perdisk=False) + self.diskIoBytes = v[2] + v[3] + self.diskIoIcon = Gtk.StatusIcon() + self.diskIoIcon.set_title("hwmon 5 disk i/o") + self.diskIoIcon.connect("popup-menu", self.rightClickEvent) + self.diskIoIcon.connect("activate", self.leftClickEvent) + self.diskIoIcon.set_visible(True) + + def updateDiskIo(self): + if not diskIoEnabled: + return + v = psutil.disk_io_counters(perdisk=False) + v = v[2] + v[3] + delta = v - self.diskIoBytes + self.diskIoBytes = v + self.diskIo.append(delta / 1.0e6 / 10) + self.diskIo.pop(0) + partitions = psutil.disk_partitions(all=False) + strPartitions = "" + for part in psutil.disk_partitions(all=False): + if 'cdrom' in part.opts or part.fstype == '': + continue + usage = psutil.disk_usage(part.mountpoint) + strPartitions += "\n%s %d%% of %s (%s)" % (part.mountpoint, int(usage.percent), bytes2human(usage.total), part.fstype) + self.diskIoIcon.set_tooltip_text("Disk I/O: %.1f MB/s\n%s" % (delta / 1.0e6 / 10, strPartitions)) + + def drawDiskIo(self): + if not diskIoEnabled: + return + self.drawGraph(self.diskIo, self.diskIoIcon, bgCol, fgDiskIo, diskIoScale) + self.drawAlert(self.diskIo, self.diskIoIcon, fgAlert, diskIoAlertMax, diskIoScale) + + def drawGraph(self, graph, icon, bgCol, fgCol, max=100): + bg = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, w, h) + bg.fill(bgCol) + + surface = cairo.ImageSurface(cairo.FORMAT_RGB24, w, h) + cr = cairo.Context(surface) + cr.set_source_rgba(fgCol[2], fgCol[1], fgCol[0], fgCol[3]) + for x in range(w): + y = int(round(graph[x]/max * h)) + if y: + cr.move_to(x, h) + cr.line_to(x, h - y) + cr.stroke() + + pixbuf = GdkPixbuf.Pixbuf.new_from_data(surface.get_data(), GdkPixbuf.Colorspace.RGB, True, 8, w, h, w * 4, None, None) + pixbuf.composite(bg, 0, 0, w, h, 0, 0, 1, 1, GdkPixbuf.InterpType.NEAREST, 255) + icon.set_from_pixbuf(bg) + + def drawAlert(self, graph, icon, fgCol, threshold, max=100): + if graph[-1] < threshold or not self.alertState: + return + bg = icon.get_pixbuf() + fg = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, w, h) + fg.fill(fgCol) + fg.composite(bg, 0, 0, w, h, 0, 0, 1, 1, GdkPixbuf.InterpType.NEAREST, 255) + icon.set_from_pixbuf(bg) + + def update(self): + self.updateCpus() + self.updateRam() + self.updateSwap() + self.updateNet() + self.updateDiskIo() + self.drawCpus() + self.drawRam() + self.drawSwap() + self.drawNet() + self.drawDiskIo() + return True + + +if __name__ == '__main__': + HardwareMonitor() + Gtk.main()