Skip to content

Instantly share code, notes, and snippets.

@gsuberland
Created June 26, 2020 19:08
Show Gist options
  • Save gsuberland/dcd90dcfebcd889cb414dad0f78ce26a to your computer and use it in GitHub Desktop.
Save gsuberland/dcd90dcfebcd889cb414dad0f78ce26a to your computer and use it in GitHub Desktop.
FreeNAS Disk Temperature Monitoring
#!/usr/local/bin/python3
import curses
from curses import wrapper
import math
import time
import psutil
from pySMART import DeviceList, Device
REFRESH_INTERVAL = 10
REFRESH_TICK_TIME = 0.05
ESCAPE_KEY = 27
COL_WHITE_ON_BLACK = 10
COL_BLACK_ON_GREEN = 2
COL_BLUE_ON_BLACK = 3
COL_CYAN_ON_BLACK = 4
COL_GREEN_ON_BLACK = 5
COL_YELLOW_ON_BLACK = 6
COL_RED_ON_BLACK = 7
TEMP_THRESHOLDS = { 'nvme': [75, 85], 'nvme_max': [70, 80], 'ssd': [45, 55], 'hdd': [50, 60] }
TEMP_COLOURS = [COL_GREEN_ON_BLACK, COL_YELLOW_ON_BLACK, COL_RED_ON_BLACK]
class FreeNasMonitor:
stdscr = None
smart = None
def __init__(self, stdscr):
self.stdscr = stdscr
self.stdscr.clear()
# put the window into no-delay mode
# in this mode calls to stuff like getkey() are non-blocking
self.stdscr.nodelay(True)
# enable keypad mode
self.stdscr.keypad(True)
# hide the cursor
curses.curs_set(False)
# set up colours
curses.start_color()
curses.init_pair(COL_WHITE_ON_BLACK, curses.COLOR_WHITE, curses.COLOR_BLACK)
curses.init_pair(COL_BLACK_ON_GREEN, curses.COLOR_BLACK, curses.COLOR_GREEN)
curses.init_pair(COL_CYAN_ON_BLACK, curses.COLOR_CYAN, curses.COLOR_BLACK)
curses.init_pair(COL_BLUE_ON_BLACK, curses.COLOR_BLUE, curses.COLOR_BLACK)
curses.init_pair(COL_GREEN_ON_BLACK, curses.COLOR_GREEN, curses.COLOR_BLACK)
curses.init_pair(COL_YELLOW_ON_BLACK, curses.COLOR_YELLOW, curses.COLOR_BLACK)
curses.init_pair(COL_RED_ON_BLACK, curses.COLOR_RED, curses.COLOR_BLACK)
# apply default colours
self.stdscr.attrset(curses.A_NORMAL | curses.color_pair(COL_WHITE_ON_BLACK))
self.stdscr.attron(curses.A_NORMAL | curses.color_pair(COL_WHITE_ON_BLACK))
#if curses.can_change_color():
# self.stdscr.addstr(1, 0, '(enhanced colour mode)')
#else:
# self.stdscr.addstr(1, 0, '(normal colour mode)')
#self.stdscr.addstr(2, 0, 'there are {} colours available'.format(curses.COLORS))
# enumerate devices
self.stdscr.addstr(0, 0, 'Enumerating SMART devices...')
self.stdscr.refresh()
self.smart = DeviceList()
self.stdscr.clear()
def begin(self):
# refresh loop
last_refresh_time = 0
while True:
# has a refresh interval passed? if so, refresh
#stdscr.addstr(0, 0, '{}'.format(time.perf_counter()))
if time.perf_counter() - last_refresh_time >= REFRESH_INTERVAL:
last_refresh_time = time.perf_counter()
self.refresh()
# delay for one tick
time.sleep(REFRESH_TICK_TIME)
key = self.stdscr.getch()
if key != -1:
self.handle_input(key)
def refresh(self):
#self.stdscr.addstr(0, 0, '{}'.format(time.perf_counter()))
self.draw_header()
max_y, max_x = self.stdscr.getmaxyx()
name_size = 10
value_size = 8
gap_size = 4
total_pad = name_size + value_size + gap_size
col_width = math.trunc(max_x / 2.0)
graph_width = math.trunc((max_x / 2.0) - total_pad)
col = 0
line = 1
for device in self.smart.devices:
device.update()
#temperatures = ""
#if len(device.temperatures) > 0:
# for sensor in device.temperatures:
# temperatures += "{}\t".format(device.temperatures[sensor])
#else:
# temperatures = '{}'.format(device.temperature)
#self.stdscr.addstr(line, 0, '{} [{} {}]: {}'.format(device.name, device.model, device.serial, temperatures))
# work out what kind of device we've got
threshold_id = 'hdd'
if 'nvme' in device.name:
threshold_id = 'nvme'
elif device.is_ssd:
threshold_id = 'ssd'
# add the basic temperature value
temps = []
temps.append((threshold_id, device.temperature))
# if there are extended (nvme) temperature values, add the max too
if len(device.temperatures) > 0:
max_temp = -9999
for sensor in device.temperatures:
max_temp = max(max_temp, device.temperatures[sensor])
temps.append(('nvme_max', max_temp))
t = 0
for temp_data in temps:
threshold_id, temp = temp_data
self.stdscr.addstr(line, col * col_width, device.name, curses.A_BOLD)
self.stdscr.addstr('.{}'.format(t), curses.color_pair(COL_BLUE_ON_BLACK) | curses.A_BOLD)
colour = TEMP_COLOURS[0]
thresholds = TEMP_THRESHOLDS[threshold_id]
if temp >= thresholds[1]:
colour = TEMP_COLOURS[2]
elif temp >= thresholds[0]:
colour = TEMP_COLOURS[1]
#self.stdscr.addstr(line, 150, '{}'.format(colour))
self.draw_bar_minmax(line, name_size + (col * col_width), graph_width, 10, 110, temp, '°C', colour)
t += 1
col += 1
if col > 1:
line += 1
col = 0
#cpu_line = 1
#cpu_idx = 0
#cpus = psutil.cpu_percent(0, True)
#for cpu in cpus:
# stdscr.addstr(cpu_line, 0, 'CPU{}: {}'.format(cpu_idx, cpu))
# draw_bar(stdscr, 0, cpu_line, 40, cpu)
# cpu_line += 1
# cpu_idx += 1
self.stdscr.refresh()
def draw_header(self):
height, width = self.stdscr.getmaxyx()
message = 'Disk Stats [' + time.strftime('%H:%M:%S', time.localtime()) + ']'
msglen = len(message)
self.stdscr.addstr(0, 0, ' ' + message + ' ' * (width - msglen - 1), curses.color_pair(COL_BLACK_ON_GREEN))
def draw_bar_minmax(self, y, x, width, min, max, value, unit, colour):
self.stdscr.addstr(y, x, '[', curses.color_pair(COL_CYAN_ON_BLACK) | curses.A_BOLD)
self.stdscr.addstr(y, x + (width - 1), ']', curses.color_pair(COL_CYAN_ON_BLACK) | curses.A_BOLD)
range = max - min
scale = (width - 2) / range
size = (value - min) * scale
bars = math.trunc(size)
self.stdscr.addstr(y, x + 1, ' ' * (width - 2), curses.color_pair(colour))
self.stdscr.addstr(y, x + 1, '|' * bars, curses.color_pair(colour) | curses.A_BOLD)
self.stdscr.addstr(y, x + width + 1, '{}{}'.format(value, unit))
def draw_bar_percent(self, x, y, width, percent):
self.stdscr.addstr(y, x, '[' + ' ' * (width - 2) + ']')
self.stdscr.addstr(y, x + width + 2, '{}'.format(percent))
value = (width - 2) * (percent / 100.0)
bars = math.trunc(value)
self.stdscr.addstr(y, x + 1, '|' * bars)
def handle_input(self, key):
if key == ESCAPE_KEY:
exit()
def main(stdscr):
monitor = FreeNasMonitor(stdscr)
monitor.begin()
wrapper(main)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment