127 lines
3.7 KiB
Python
127 lines
3.7 KiB
Python
#!/usr/bin/env python3
|
|
"""WebSocket server that sends real Pi system stats every 2 seconds.
|
|
|
|
Drop-in replacement for mock_server.py. Same port (8765), same JSON schema,
|
|
same 2s push interval. Services remain mocked until systemd integration is added.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import random
|
|
import time
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
import psutil
|
|
import websockets
|
|
|
|
# Prime the CPU percent counter (first call always returns 0.0)
|
|
psutil.cpu_percent(interval=None)
|
|
|
|
# Network baseline for delta calculation
|
|
_prev_net = psutil.net_io_counters()
|
|
_prev_net_time = time.monotonic()
|
|
|
|
|
|
def _get_cpu_temp() -> float:
|
|
"""Read CPU temperature with fallback for different Pi OS versions."""
|
|
try:
|
|
temps = psutil.sensors_temperatures()
|
|
if "cpu_thermal" in temps and temps["cpu_thermal"]:
|
|
return round(temps["cpu_thermal"][0].current, 1)
|
|
except (AttributeError, KeyError):
|
|
pass
|
|
|
|
# Fallback: read sysfs directly (value is in millidegrees)
|
|
thermal_path = Path("/sys/class/thermal/thermal_zone0/temp")
|
|
try:
|
|
millidegrees = int(thermal_path.read_text().strip())
|
|
return round(millidegrees / 1000.0, 1)
|
|
except (FileNotFoundError, ValueError, PermissionError):
|
|
return 0.0
|
|
|
|
|
|
def _get_net_throughput() -> tuple[float, float]:
|
|
"""Calculate network rx/tx in kbps since last call."""
|
|
global _prev_net, _prev_net_time
|
|
|
|
now = time.monotonic()
|
|
current = psutil.net_io_counters()
|
|
elapsed = now - _prev_net_time
|
|
|
|
if elapsed <= 0:
|
|
return 0.0, 0.0
|
|
|
|
rx_kbps = round((current.bytes_recv - _prev_net.bytes_recv) * 8 / (elapsed * 1000), 1)
|
|
tx_kbps = round((current.bytes_sent - _prev_net.bytes_sent) * 8 / (elapsed * 1000), 1)
|
|
|
|
_prev_net = current
|
|
_prev_net_time = now
|
|
|
|
return rx_kbps, tx_kbps
|
|
|
|
|
|
def _mock_services() -> list[dict]:
|
|
"""Mocked service status — same logic as mock_server.py."""
|
|
return [
|
|
{"name": "docker", "status": random.choice(["running", "running", "running", "stopped"])},
|
|
{"name": "pihole", "status": random.choice(["running", "running", "running", "stopped"])},
|
|
{"name": "nginx", "status": random.choice(["running", "running", "stopped"])},
|
|
{"name": "sshd", "status": "running"},
|
|
]
|
|
|
|
|
|
def _local_time_fields() -> dict:
|
|
"""Current local time as broken-down fields for RTC sync."""
|
|
now = datetime.now()
|
|
return {
|
|
"y": now.year,
|
|
"mo": now.month,
|
|
"d": now.day,
|
|
"h": now.hour,
|
|
"m": now.minute,
|
|
"s": now.second,
|
|
}
|
|
|
|
|
|
def generate_stats() -> dict:
|
|
mem = psutil.virtual_memory()
|
|
disk = psutil.disk_usage("/")
|
|
rx_kbps, tx_kbps = _get_net_throughput()
|
|
|
|
return {
|
|
"cpu_pct": psutil.cpu_percent(interval=None),
|
|
"mem_pct": round(mem.percent, 1),
|
|
"mem_used_mb": int(mem.used // (1024 * 1024)),
|
|
"disk_pct": round(disk.percent, 1),
|
|
"cpu_temp": _get_cpu_temp(),
|
|
"uptime_hrs": round((time.time() - psutil.boot_time()) / 3600, 1),
|
|
"net_rx_kbps": rx_kbps,
|
|
"net_tx_kbps": tx_kbps,
|
|
"services": _mock_services(),
|
|
"timestamp": int(time.time()),
|
|
"local_time": _local_time_fields(),
|
|
}
|
|
|
|
|
|
async def handler(websocket):
|
|
addr = websocket.remote_address
|
|
print(f"Client connected: {addr}")
|
|
try:
|
|
while True:
|
|
stats = generate_stats()
|
|
await websocket.send(json.dumps(stats))
|
|
await asyncio.sleep(2)
|
|
except websockets.ConnectionClosed:
|
|
print(f"Client disconnected: {addr}")
|
|
|
|
|
|
async def main():
|
|
print("Pi stats server starting on ws://0.0.0.0:8765")
|
|
async with websockets.serve(handler, "0.0.0.0", 8765):
|
|
await asyncio.Future() # run forever
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|