Делюсь еще одной полезной штукой из личных разработок, на этот раз на тему мониторинга температуры в компьютере.
~120 строк на Python.

Задача
Честно говоря, задача мониторинга температуры это такой своеобразный «неуловимый Джо» — ненужная мелкая ерунда, реализуемая в больших системах для галочки и по остаточному принципу.
Ну и конечно отображение температуры с датчиков персонального компьютера давным давно реализовано и присутствует в виде виджета во всех основных графических окружениях и для любых ОС.
Чаще всего это выглядит как-то так:

Это все к вопросу об уникальности, важности и полезности решения, варианты есть, их много, но как всегда есть нюансы. Кстати одна из первых разработанных автором систем была как раз на тему мониторинга температуры у парка серверов:
дело было давно и сервера (как и сетевое оборудование) стояли тогда не в охлаждаемых и чистых серверных а где придется.
С соответствующими последствиями по перегреву. Выглядело оно как-то так:

А так выглядит виновник сегодняшнего торжества:

Исходный код можно посмотреть чуть ниже в статье или вот тут в виде gist. Теперь рассказываю подробнее как оно все работает.
Кстати думаю для многих станет сюрпризом, что X-сервер позволяет рисовать графические элементы и текст в чужих окнах.
Без предупреждения, без индикации, без каких-то отдельных прав и так далее.
Реализация
Поскольку у автора используется тот еще зоопарк разных систем, хотелось сделать максимально переносимое решение. Хардкодить все на чистом С я посчитал излишним, поэтому был взят Python и библиотека Xlib, которая есть везде, куда еще не добрались проклятые зумеры юные специалисты с Wayland.
К великому сожалению, оказалось что знаменитая связка Tcl/Tk не умеет работать с root window и использовать их для этой задачи не получится.
Именно поэтому был взят Python, да.
Собственно эта библиотека является единственной внешней зависимостью в этом проекте, для FreeBSD устанавливается вот так:
pkg install py311-python-xlib
Аналогичные пакеты есть в любом линуксе и BSD. Теперь стоит рассказать про сами датчики. Датчики температуры в случае FreeBSD отдают свои значения через sysctl и требуют подгрузки специального модуля ядра.
Чаще всего это будет coretemp:
kldload coretemp
Чтение значений датчиков будет выглядеть как-то так:

Для процессоров AMD есть отдельный модуль amdtemp:
kldload amdtemp
Названия датчиков отличаются, поэтому для чтения используется немного другой паттерн:
sysctl dev.amdtemp.0 |grep core
В случае линукса температуру с датчиков можно получить из специальной виртуальной файловой системы /dev:
cat /sys/class/thermal/thermal_zone*/temp
Так это выглядит в работе:

Исходный код
Ниже представлен полный исходный код моей утилиты, технически это shebang — самозапускаемый скрипт на Python, поэтому его нужно сохранить с расширением .sh и поставить бит запуска.
Собственно код:
#!/usr/bin/env python3 import Xlib from Xlib import display, X # display и X - не импортируются автоматически import subprocess,time,logging #настройки логирования logging.basicConfig(level=logging.INFO) #logging.basicConfig(level=logging.DEBUG) # координаты на экране для отображения POS_X = 150 POS_Y = 50 # для FreeBSD и машины с AMD PATTERN = 'sysctl dev.amdtemp.0 |grep core' # для FreeBSD с модулем coretemp #PATTERN = 'sysctl dev.cpu |grep temperature' # для Linux #PATTERN = "cat /sys/class/thermal/thermal_zone*/temp | awk '{ print \"temp: \" ($1 / 1000) \"C\" }'" # шрифт FONT = '-misc-fixed-medium-r-normal--13-120-75-75-c-70-iso8859-1' # период обновления REFRESH_SECS = 15 last_dim = [0,0] # определение рабочего стола # d - display (экран)s def get_root_window(d): screen = d.screen() # root window - рабочий стол по-умолчанию root = screen.root # Получаем ID всех окон верхнего уровня windowIDs = root.get_full_property(d.intern_atom('_NET_CLIENT_LIST'), X.AnyPropertyType).value logging.debug('Found %d windows.',len(windowIDs)) for windowID in windowIDs: # Create a window object from the ID to access its properties window = d.create_resource_object('window', windowID) try: # Get the window title (WM_NAME or _NET_WM_NAME) # Use get_wm_name() for simplicity, # or look up EWMH properties for better compatibility window_name = window.get_wm_name() if window_name: # в продвинутых DE вроде KDE/Xfce за рабочий стол отвечает # отдельное окно и рисовать придется в нем if 'Desktop' in window_name: logging.debug("Found desktop ID: %d - Name: %s", windowID,window_name) return window else: # Бывает, что у окна нет заголовка logging.debug("ID: %d - Name: None (no WM_NAME property)", windowID) except X.BadWindow: # Обработка ситуации, когда считываемое окно больше не существует logging.debug("ID: %d - Window no longer exists",windowID) # если отдельного окна с именем Desktop не обнаружено - рисуем # прямо на root window return root # очистка области def clear_rect(msg): global last_dim # если есть сохраненные размеры - используем их if last_dim[0] > 0: root.fill_rectangle(gc2, POS_X,POS_Y-last_dim[1], last_dim[0]-20, last_dim[1]+5) else: # расчет размеров надписи в пикселях text_extents = font.query_text_extents(msg) tw = text_extents.overall_width th = text_extents.font_ascent + text_extents.font_descent # очистка происходит через отрисовку черного прямоугольника # clear_area плохо работает с KDE root.fill_rectangle(gc2, POS_X,POS_Y-th, tw-20, th+5) # запоминаем размеры надписи для следующей очистки last_dim = [tw,th] # отрисовка сообщения на экране # msg - текст сообщения def draw_message(msg): # очистка области clear_rect(msg) # отрисовка текста root.draw_text(gc, POS_X, POS_Y, msg) display.flush() # инициализация подключения к Х-серверу display = Xlib.display.Display() screen = display.screen() root = get_root_window(display) # Access the window ID (an integer) root_id = root.id logging.debug("Root window ID: %d",root_id) # для реакции на события root.change_attributes(event_mask=X.ExposureMask) # "adds" this event mask # создание графического контекста, белый текст на черном фоне gc = root.create_gc(foreground = screen.white_pixel, background = screen.black_pixel) # дополнительный контекст для заливки области черным colormap = screen.default_colormap color = colormap.alloc_named_color('black') gc2 = root.create_gc(foreground=color.pixel) # загружаем шрифт, которым будет отрисовываться текст # если шрифт с таким названием не будет найден - вылетит ошибка try: font = display.open_font(FONT) gc.font = font.id except Exception as e: logging.exception(e) exit(1) try: # бесконечный цикл, в котором происходит все действо while 1: # запуск процесса для получения значений датчиков process = subprocess.Popen(PATTERN, shell=True, text=True, stdout=subprocess.PIPE) # в этой переменной будет массив строк со значениями stdout_list = process.communicate()[0].split('\n') out = '' # делаем чистку for s in stdout_list: # убираем ошибочные строки, если нет : - нет и значения if ':' not in s: continue kv = s.split(':') # добавляем запятую в качестве разделителя if len(out) > 0: out+= ',' # добавляем значение датчика в строку out+= kv[1] logging.debug(out) # отрисовываем полученную строку draw_message(out.encode()) # задержка между итерациями time.sleep(REFRESH_SECS) except KeyboardInterrupt: # при нажатии Ctrl-C делаем очистку области экрана, где # происходила отрисовка виджета x = POS_X // 2 y = POS_Y // 2 root.clear_area(x,y,last_dim[0]+x,last_dim[1]+y,True) display.flush()
Как видите тут нет ООП и нет многопоточности — реализация максимально упрощена. Также не стал городить считывание параметров, поэтому ключевые настройки находятся в самом скрипте.
Настройка логирования:
logging.basicConfig(level=logging.INFO) #logging.basicConfig(level=logging.DEBUG)
Координаты для отображения на экране (левый нижний угол):
POS_X = 150 POS_Y = 50
Выбор паттерна для считывания значений датчиков:
# для FreeBSD и машины с AMD PATTERN = 'sysctl dev.amdtemp.0 |grep core' # для FreeBSD с модулем coretemp PATTERN = 'sysctl dev.cpu |grep temperature' # для Linux PATTERN = "cat /sys/class/thermal/thermal_zone*/temp | awk '{ print \"temp: \" ($1 / 1000) \"C\" }'"
Шрифт, которым будут отображаться значения температуры, указывается в специальном формате:
FONT = '-misc-fixed-medium-r-normal--13-120-75-75-c-70-iso8859-1'
Частота обновлений в секундах:
REFRESH_SECS = 15
Так виджет выглядит на Ubuntu и Xfce:

Очко сотрудников СБ только что заиграло новыми красками.
