├── get_proxmox_data.py ├── README.md ├── main.py └── draw_number.py /get_proxmox_data.py: -------------------------------------------------------------------------------- 1 | import urequests as requests 2 | import ujson as json 3 | 4 | # Proxmox API credentials 5 | PROXMOX_HOST = "https://192.168.1.228:8006" # change to your proxmox IP address 6 | API_TOKEN = "xxx@xxx!xxx=xx-xxxx-xxx" # change to your proxmox API token 7 | 8 | # Function to make requests to the Proxmox API 9 | def proxmox_get(endpoint): 10 | url = f"{PROXMOX_HOST}/api2/json{endpoint}" 11 | headers = { 12 | 'Authorization': f'PVEAPIToken={API_TOKEN}', 13 | 'Accept': 'application/json', 14 | } 15 | 16 | try: 17 | # Make the GET request to the API 18 | response = requests.get(url, headers=headers) 19 | if response.status_code == 200: 20 | return response.json() 21 | else: 22 | print(f"Error: {response.status_code} - {response.text}") 23 | return None 24 | except Exception as e: 25 | print(f"Request failed: {e}") 26 | return None 27 | 28 | # Get usage statistics for a specific node 29 | def get_node_usage(node_name): 30 | endpoint = f"/nodes/{node_name}/status" 31 | data = proxmox_get(endpoint) 32 | if data is not None: 33 | return {"cpu": data['data']['cpu'], "memory": data['data']['memory']['used'] / data['data']['memory']['total'], "storage": data['data']['rootfs']['used'] / data['data']['rootfs']['total']} 34 | return {"cpu": -1, "memory": -1, "storage": -1} 35 | 36 | 37 | if __name__ == '__main__': 38 | print(get_node_usage('proxmox')) 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pico-proxmox-monitor 2 | A project I made for monitoring Proxmox system usage! 3 | 4 | # Hardware 5 | - Raspberry Pi Pico 2W (Pico W might be fine, haven't tested) 6 | - [64x32 LED Matrix](https://www.adafruit.com/product/2279) 7 | - [Power supply](https://www.adafruit.com/product/1466) 8 | - [Hub75 Hat](https://www.digikey.com/en/products/detail/adafruit-industries-llc/3211/8535237?utm_adgroup=&utm_source=google&utm_medium=cpc&utm_campaign=PMax%20Shopping_Product_Low%20ROAS%20Categories&utm_term=&utm_content=&utm_id=go_cmp-20243063506_adg-_ad-__dev-c_ext-_prd-8535237_sig-CjwKCAjwx4O4BhAnEiwA42SbVJyumFphNeKcb4d-vKaA66kscJa-CfjE17rJQ32VC_XsbUS2cKx_gBoCTdoQAvD_BwE&gad_source=1&gclid=CjwKCAjwx4O4BhAnEiwA42SbVJyumFphNeKcb4d-vKaA66kscJa-CfjE17rJQ32VC_XsbUS2cKx_gBoCTdoQAvD_BwE) (This may not be strictly necessary. I had it on hand since I was previously using a Rpi 4 to drive the display. Obviously it's not compatible with the Pico so I am using some jumper cables to connect to the pinholes.) 9 | - Custom laser-cut plywood box. I don't have the model for this anymore. I made it from scratch a while ago, but since then I discovered [this website](https://en.makercase.com/#/) which might be a good resource for this. 10 | 11 | # Usage 12 | - Flash your Pico W (or 2W) with the [latest Pimoroni MicroPython fork](https://github.com/pimoroni/pimoroni-pico/releases/) (I am currently running pico-v1.23.0-1-pimoroni-micropython.uf2). This includes the hub75 module needed for driving the display. 13 | - Then connect via USB and open your favorite editor, like Thonny, and send the 3 Python files over. 14 | - Modify main.py to include your WIFI SSID and password. 15 | - Modify get_proxmox_data.py to include your Proxmox instance's IP address and [API token](https://pve.proxmox.com/wiki/Proxmox_VE_API). 16 | - All set! 17 | 18 | # Issues? 19 | [Open an issue here, and I will try to respond.](https://github.com/R2bEEaton/pico-proxmox-monitor/issues) I made this for fun, feel free to edit it. 20 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import hub75 2 | import time 3 | import network 4 | import ubinascii 5 | import urequests 6 | import qrcode 7 | import ujson 8 | import gc 9 | from get_proxmox_data import get_node_usage 10 | from draw_number import * 11 | 12 | HEIGHT = 32 13 | WIDTH = 64 14 | 15 | h75 = hub75.Hub75(WIDTH, HEIGHT, stb_invert=False) 16 | h75.start() 17 | 18 | 19 | def blink(n=1, c=(255, 0, 0), delay=1.0): 20 | global h75 21 | for _ in range(n): 22 | h75.set_pixel(0, 0, *c) 23 | time.sleep(0.1) 24 | h75.set_pixel(0, 0, 0, 0, 0) 25 | time.sleep(delay) 26 | 27 | 28 | def error(n): 29 | global h75 30 | while True: 31 | for _ in range(n): 32 | h75.set_pixel(0, 0, 255, 0, 0) 33 | time.sleep(0.1) 34 | h75.set_pixel(0, 0, 0, 0, 0) 35 | time.sleep(0.25) 36 | time.sleep(1) 37 | 38 | 39 | blink() 40 | 41 | ssid = 'YOURSSID' 42 | password = 'YOURPASSWORD' 43 | 44 | wlan = network.WLAN(network.STA_IF) 45 | mac = ubinascii.hexlify(network.WLAN().config('mac')).decode() 46 | print(mac) 47 | del mac 48 | wlan.active(True) 49 | wlan.connect(ssid, password) 50 | 51 | # Wait for connect or fail 52 | max_wait = 10 53 | while max_wait > 0: 54 | if wlan.status() < 0 or wlan.status() >= 3: 55 | break 56 | max_wait -= 1 57 | print('waiting for connection...') 58 | time.sleep(1) 59 | 60 | # Handle connection error 61 | if wlan.status() != 3: 62 | error(3) 63 | exit() 64 | 65 | blink(n=3, c=(0, 255, 0), delay=0.25) 66 | print('connected') 67 | status = wlan.ifconfig() 68 | print('ip = ' + status[0]) 69 | 70 | # Set QR code to IP address of PI 71 | qr = qrcode.QRCode() 72 | qr.set_text(status[0]) 73 | 74 | for x in range(qr.get_size()[0]+2): 75 | for y in range(qr.get_size()[1]+2): 76 | if qr.get_module(x-1, y-1): 77 | h75.set_pixel(x, y, 0, 0, 0) 78 | else: 79 | h75.set_pixel(x, y, 255, int(255*(x/qr.get_size()[0]+1)), int(255*(y/qr.get_size()[1]+1))) 80 | del qr 81 | 82 | 83 | def draw_line(x0, y0, x1, y1, r, g, b): 84 | # Line drawing code, stolen from ChatGPT 85 | # Calculate differences 86 | dx = abs(x1 - x0) 87 | dy = abs(y1 - y0) 88 | 89 | # Determine the direction of the line 90 | sx = 1 if x0 < x1 else -1 91 | sy = 1 if y0 < y1 else -1 92 | 93 | err = dx - dy 94 | 95 | while True: 96 | # Set the pixel at the current coordinates 97 | h75.set_pixel(x0, y0, r, g, b) 98 | 99 | # If we've reached the end point, break the loop 100 | if x0 == x1 and y0 == y1: 101 | break 102 | 103 | # Calculate the error and adjust x or y accordingly 104 | e2 = 2 * err 105 | if e2 > -dy: 106 | err -= dy 107 | x0 += sx 108 | if e2 < dx: 109 | err += dx 110 | y0 += sy 111 | 112 | 113 | gc.collect() 114 | print(gc.mem_free()) 115 | 116 | queue_len = 16 117 | buckets = {"cpu": queue_len * [-1], "memory": queue_len * [-1], "storage": queue_len * [-1]} 118 | 119 | while True: 120 | data = get_node_usage('proxmox') 121 | h75.clear() 122 | for k, v in data.items(): 123 | buckets[k].pop(0) 124 | buckets[k].append(v) 125 | try: 126 | min_value = min(v for v in buckets[k] if v != -1) 127 | max_value = max(v for v in buckets[k]) 128 | except: 129 | continue 130 | for x, hist in enumerate(list(zip(buckets[k], buckets[k][1:]))): 131 | if hist[0] == -1 or hist[1] == -1: 132 | continue 133 | p1 = 0.5 if max_value == min_value else (hist[0] - min_value) / (max_value - min_value) 134 | p2 = 0.5 if max_value == min_value else (hist[1] - min_value) / (max_value - min_value) 135 | if k == "storage": 136 | draw_line(x * 3, 9 - round(p1 * 9), x * 3 + 3, 9 - round(p2 * 9), 127, 127, 127) 137 | draw_number(h75, str(v * 100)[:4], 49, 3, 255, 255, 255) 138 | elif k == "memory": 139 | draw_line(x * 3, 20 - round(p1 * 9), x * 3 + 3, 20 - round(p2 * 9), 255, 0, 0) 140 | draw_number(h75, str(v * 100)[:4], 49, 14, 255, 0, 0) 141 | elif k == "cpu": 142 | draw_line(x * 3, 31 - round(p1 * 9), x * 3 + 3, 31 - round(p2 * 9), 0, 255, 255) 143 | draw_number(h75, str(v * 100)[:4], 49, 25, 0, 255, 255) 144 | #print(k, min_value, max_value) 145 | 146 | #print() 147 | gc.collect() 148 | time.sleep(0.1) 149 | -------------------------------------------------------------------------------- /draw_number.py: -------------------------------------------------------------------------------- 1 | def d0(h75, x, y, r, g, b): 2 | h75.set_pixel(x + 0, y + 0, r, g, b) 3 | h75.set_pixel(x + 1, y + 0, r, g, b) 4 | h75.set_pixel(x + 2, y + 0, r, g, b) 5 | h75.set_pixel(x + 0, y + 1, r, g, b) 6 | h75.set_pixel(x + 2, y + 1, r, g, b) 7 | h75.set_pixel(x + 0, y + 2, r, g, b) 8 | h75.set_pixel(x + 2, y + 2, r, g, b) 9 | h75.set_pixel(x + 0, y + 3, r, g, b) 10 | h75.set_pixel(x + 2, y + 3, r, g, b) 11 | h75.set_pixel(x + 0, y + 4, r, g, b) 12 | h75.set_pixel(x + 1, y + 4, r, g, b) 13 | h75.set_pixel(x + 2, y + 4, r, g, b) 14 | return 3 15 | 16 | def d1(h75, x, y, r, g, b): 17 | h75.set_pixel(x + 0, y + 0, r, g, b) 18 | h75.set_pixel(x + 0, y + 1, r, g, b) 19 | h75.set_pixel(x + 0, y + 2, r, g, b) 20 | h75.set_pixel(x + 0, y + 3, r, g, b) 21 | h75.set_pixel(x + 0, y + 4, r, g, b) 22 | return 1 23 | 24 | def d2(h75, x, y, r, g, b): 25 | h75.set_pixel(x + 0, y + 0, r, g, b) 26 | h75.set_pixel(x + 1, y + 0, r, g, b) 27 | h75.set_pixel(x + 2, y + 0, r, g, b) 28 | h75.set_pixel(x + 2, y + 1, r, g, b) 29 | h75.set_pixel(x + 0, y + 2, r, g, b) 30 | h75.set_pixel(x + 1, y + 2, r, g, b) 31 | h75.set_pixel(x + 2, y + 2, r, g, b) 32 | h75.set_pixel(x + 0, y + 3, r, g, b) 33 | h75.set_pixel(x + 0, y + 4, r, g, b) 34 | h75.set_pixel(x + 1, y + 4, r, g, b) 35 | h75.set_pixel(x + 2, y + 4, r, g, b) 36 | return 3 37 | 38 | def d3(h75, x, y, r, g, b): 39 | h75.set_pixel(x + 0, y + 0, r, g, b) 40 | h75.set_pixel(x + 1, y + 0, r, g, b) 41 | h75.set_pixel(x + 2, y + 0, r, g, b) 42 | h75.set_pixel(x + 2, y + 1, r, g, b) 43 | h75.set_pixel(x + 0, y + 2, r, g, b) 44 | h75.set_pixel(x + 1, y + 2, r, g, b) 45 | h75.set_pixel(x + 2, y + 2, r, g, b) 46 | h75.set_pixel(x + 2, y + 3, r, g, b) 47 | h75.set_pixel(x + 0, y + 4, r, g, b) 48 | h75.set_pixel(x + 1, y + 4, r, g, b) 49 | h75.set_pixel(x + 2, y + 4, r, g, b) 50 | return 3 51 | 52 | def d4(h75, x, y, r, g, b): 53 | h75.set_pixel(x + 0, y + 0, r, g, b) 54 | h75.set_pixel(x + 2, y + 0, r, g, b) 55 | h75.set_pixel(x + 0, y + 1, r, g, b) 56 | h75.set_pixel(x + 2, y + 1, r, g, b) 57 | h75.set_pixel(x + 0, y + 2, r, g, b) 58 | h75.set_pixel(x + 1, y + 2, r, g, b) 59 | h75.set_pixel(x + 2, y + 2, r, g, b) 60 | h75.set_pixel(x + 2, y + 3, r, g, b) 61 | h75.set_pixel(x + 2, y + 4, r, g, b) 62 | return 3 63 | 64 | def d5(h75, x, y, r, g, b): 65 | h75.set_pixel(x + 0, y + 0, r, g, b) 66 | h75.set_pixel(x + 1, y + 0, r, g, b) 67 | h75.set_pixel(x + 2, y + 0, r, g, b) 68 | h75.set_pixel(x + 0, y + 1, r, g, b) 69 | h75.set_pixel(x + 0, y + 2, r, g, b) 70 | h75.set_pixel(x + 1, y + 2, r, g, b) 71 | h75.set_pixel(x + 2, y + 2, r, g, b) 72 | h75.set_pixel(x + 2, y + 3, r, g, b) 73 | h75.set_pixel(x + 0, y + 4, r, g, b) 74 | h75.set_pixel(x + 1, y + 4, r, g, b) 75 | h75.set_pixel(x + 2, y + 4, r, g, b) 76 | return 3 77 | 78 | def d6(h75, x, y, r, g, b): 79 | h75.set_pixel(x + 0, y + 0, r, g, b) 80 | h75.set_pixel(x + 1, y + 0, r, g, b) 81 | h75.set_pixel(x + 2, y + 0, r, g, b) 82 | h75.set_pixel(x + 0, y + 1, r, g, b) 83 | h75.set_pixel(x + 0, y + 2, r, g, b) 84 | h75.set_pixel(x + 1, y + 2, r, g, b) 85 | h75.set_pixel(x + 2, y + 2, r, g, b) 86 | h75.set_pixel(x + 0, y + 3, r, g, b) 87 | h75.set_pixel(x + 2, y + 3, r, g, b) 88 | h75.set_pixel(x + 0, y + 4, r, g, b) 89 | h75.set_pixel(x + 1, y + 4, r, g, b) 90 | h75.set_pixel(x + 2, y + 4, r, g, b) 91 | return 3 92 | 93 | def d7(h75, x, y, r, g, b): 94 | h75.set_pixel(x + 0, y + 0, r, g, b) 95 | h75.set_pixel(x + 1, y + 0, r, g, b) 96 | h75.set_pixel(x + 2, y + 0, r, g, b) 97 | h75.set_pixel(x + 2, y + 1, r, g, b) 98 | h75.set_pixel(x + 2, y + 2, r, g, b) 99 | h75.set_pixel(x + 2, y + 3, r, g, b) 100 | h75.set_pixel(x + 2, y + 4, r, g, b) 101 | return 3 102 | 103 | def d8(h75, x, y, r, g, b): 104 | h75.set_pixel(x + 0, y + 0, r, g, b) 105 | h75.set_pixel(x + 1, y + 0, r, g, b) 106 | h75.set_pixel(x + 2, y + 0, r, g, b) 107 | h75.set_pixel(x + 0, y + 1, r, g, b) 108 | h75.set_pixel(x + 2, y + 1, r, g, b) 109 | h75.set_pixel(x + 0, y + 2, r, g, b) 110 | h75.set_pixel(x + 1, y + 2, r, g, b) 111 | h75.set_pixel(x + 2, y + 2, r, g, b) 112 | h75.set_pixel(x + 0, y + 3, r, g, b) 113 | h75.set_pixel(x + 2, y + 3, r, g, b) 114 | h75.set_pixel(x + 0, y + 4, r, g, b) 115 | h75.set_pixel(x + 1, y + 4, r, g, b) 116 | h75.set_pixel(x + 2, y + 4, r, g, b) 117 | return 3 118 | 119 | def d9(h75, x, y, r, g, b): 120 | h75.set_pixel(x + 0, y + 0, r, g, b) 121 | h75.set_pixel(x + 1, y + 0, r, g, b) 122 | h75.set_pixel(x + 2, y + 0, r, g, b) 123 | h75.set_pixel(x + 0, y + 1, r, g, b) 124 | h75.set_pixel(x + 2, y + 1, r, g, b) 125 | h75.set_pixel(x + 0, y + 2, r, g, b) 126 | h75.set_pixel(x + 1, y + 2, r, g, b) 127 | h75.set_pixel(x + 2, y + 2, r, g, b) 128 | h75.set_pixel(x + 2, y + 3, r, g, b) 129 | h75.set_pixel(x + 0, y + 4, r, g, b) 130 | h75.set_pixel(x + 1, y + 4, r, g, b) 131 | h75.set_pixel(x + 2, y + 4, r, g, b) 132 | return 3 133 | 134 | def dd(h75, x, y, r, g, b): 135 | h75.set_pixel(x + 0, y + 4, r, g, b) 136 | return 1 137 | 138 | 139 | draw_funcs = [d0, d1, d2, d3, d4, d5, d6, d7, d8, d9, dd] 140 | 141 | 142 | def draw_number(h75, num, x, y, r, g, b): 143 | num = str(num) 144 | for i in range(len(num)): 145 | c = num[i] 146 | if c in "0123456789.": 147 | w = draw_funcs["0123456789.".index(c)](h75, x, y, r, g, b) 148 | x += w + 1 149 | 150 | if __name__ == "__main__": 151 | import hub75 152 | 153 | HEIGHT = 32 154 | WIDTH = 64 155 | 156 | h75 = hub75.Hub75(WIDTH, HEIGHT, stb_invert=False) 157 | h75.start() 158 | 159 | draw_number(h75, 1.234567890, 0, 0, 255, 255, 255) 160 | 161 | --------------------------------------------------------------------------------