#!/usr/bin/env python3
#
# oled_sysinfo.py v1.3
#
# SSD1306-driven OLED 128 x 64 display - GeeekPi Minitower
#
# for: 52Pi ABS Mini Tower ZP-0187 ZP-0172 ZP-0130 ZP-0129 ZP-0128
#
# Copyright (c) 2012- Exaga - sarpi.penthux.net 
#
# Version 1.0 : 2012-12-22 
# Version 1.1 : 2018-07-14 
# Version 1.2 : 2021-03-25 
# Version 1.3 : 2026-02-26 
#
# Released under the MIT License.
# See the LICENSE file for full license text.
#
###


# Standard Library [built into Python]
import os
import sys
import signal
import time
from pathlib import Path
from datetime import datetime
import psutil

# Third-Party Library [installed via pip]
from luma.core.render import canvas
from PIL import ImageFont
from luma.core.interface.serial import i2c
from luma.oled.device import ssd1306


# OLED screen settings
DEVICE_ID = ssd1306
I2C_BUS = 1
I2C_ADDRESS = 0x3C

# Screen refresh time [seconds]
REFRESH_TIME = 1

# OS categories
#
# 'posix' ->   Linux, Android, ChromeOS, macOS, *BSD, Solaris,
#              iOS (if running Python in an embedded context),
#              Embedded Linux systems (OpenWRT, Yocto, etc.)
# 'nt'    ->   Windows []
# 'java'  ->   Jython

# If OS doesn't match -> exit
if os.name != 'posix':
    sys.exit(f'{os.name} platform is not supported')

# Clear OLED display and exit cleanly
def shutdown(signum, frame):
    device.clear()
    sys.exit(0)

signal.signal(signal.SIGTERM, shutdown)
signal.signal(signal.SIGINT, shutdown)

# Convert bytes into human-readable values
def bytes2human(n):
    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 = int(float(n) / prefix[s])
            return '%s%s' % (value, s)
    return f"{n}B"

# Calculate uptime from datetime
def cpu_usage():
    uptime = datetime.now() - datetime.fromtimestamp(psutil.boot_time())
    days = uptime.days
    hours, remainder = divmod(uptime.seconds, 3600)
    minutes, _ = divmod(remainder, 60)
    # CPU % usage
    cpu_percent = psutil.cpu_percent(interval=None)
    return "CPU %d%% Up %dd%dh%dm" % (cpu_percent, days, hours, minutes)

# RAM usage + total + %
def mem_usage():
    usage = psutil.virtual_memory()
    return "RAM %s/%s (%d%%)" % (
        bytes2human(usage.used),
        bytes2human(usage.total),
        usage.percent
    )

# Storage usage + total + %
def disk_usage():
    usage = psutil.disk_usage('/')
    return "SD %s/%s (%d%%)" % (
        bytes2human(usage.used),
        bytes2human(usage.total),
        usage.percent
    )

# CPU temperature in degrees Celsius
def cpu_temp():
    with open("/sys/class/thermal/thermal_zone0/temp") as f:
        temp = int(f.read().strip()) / 1000
    return "TEMP %.1fC" % temp

# Set the font file
def stats(device):
    font_path = str(Path(__file__).resolve().parent.joinpath('fonts', 'DejaVuSansMono.ttf'))
    # Set font pixel size
    font = ImageFont.truetype(font_path, 9)
    # Calculate line height
    ascent, descent = font.getmetrics()
    line_height = ascent + descent
    # Draw system stats [left aligned]
    with canvas(device) as draw:
        draw.text((2, line_height * 0), cpu_usage(), font=font, fill="white")
        draw.text((2, line_height * 1), mem_usage(), font=font, fill="white")
        draw.text((2, line_height * 2), disk_usage(), font=font, fill="white")
        draw.text((2, line_height * 3), cpu_temp(), font=font, fill="white")
        # Generate centred timestamp
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        text_width = draw.textlength(timestamp, font=font)
        x = (device.width - text_width) // 2
        y = device.height - line_height
        # Position timestamp on bottom line
        draw.text((x, y), timestamp, font=font, fill="white")

# Infinite loop sleep period [seconds]
def main():
    while True:
        stats(device)
        time.sleep(REFRESH_TIME)

# Ensure script runs only when executed directly
if __name__ == "__main__":
    try:
        # Initialise display
        serial = i2c(port=I2C_BUS, address=I2C_ADDRESS)
        device = DEVICE_ID(serial)
        # Start infinite loop
        main()
    # Catch Ctrl+C
    except KeyboardInterrupt:
        pass
    # Exit infinite loop and clear display
    finally:
        device.clear()
        
#EOF<*>

