feat: expose per-group refresh plans and cache storage daily

This commit is contained in:
way
2026-10-15 09:17:20 +08:00
parent b63fa39f32
commit 174ac747c5
6 changed files with 134 additions and 18 deletions
+25 -14
View File
@@ -1,12 +1,13 @@
#!/usr/bin/env python3
import time,socket,subprocess,configparser,signal,re,argparse,json,os,platform
from pathlib import Path
APP_VERSION="3.9.0"
APP_VERSION="3.10.0"
import sys
sys.path.append("/usr/local/share/pigway-pi-control")
from shutil import disk_usage
from plugin_api import Publisher,registered,discover
from network_rates import NetworkRates
from storage_cache import StorageCache
storage_cache=StorageCache("/run/pigway-pi-control/storage-cache.json")
CFG="/etc/pigway-pi-control.conf"
STATUS_PATH="/run/pigway-pi-control/status.json"
@@ -144,7 +145,8 @@ def mem_pct():
k,v=l.split(":",1); vals[k]=int(v.split()[0])
return 100*(vals["MemTotal"]-vals["MemAvailable"])/vals["MemTotal"]
def disk_pct():
d=disk_usage("/"); return 100*d.used/d.total
d=storage_cache.sample()
return 100*d["used"]/d["total"] if d else None
def _run(args,timeout=2):
try: return subprocess.check_output(args,text=True,stderr=subprocess.DEVNULL,timeout=timeout).strip()
except Exception: return ""
@@ -326,14 +328,15 @@ def update_active_alerts(cpu,temp,mem,disk,sample_elapsed=0.0):
alert_manager.clear("TEMP_HIGH"); alert_manager.clear("TEMP_CRITICAL")
if mem_hi>=MEM_HIGH_SEC: alert_manager.request("MEMORY_HIGH",1,"MEMORY HIGH",f"USED {mem:.1f}%","CHECK PROCESS","")
else: alert_manager.clear("MEMORY_HIGH")
if disk>=DISK_CRIT:
alert_manager.request("DISK_CRITICAL",2,"DISK CRITICAL",f"USED {disk:.1f}%","FREE < 5%","CHECK STORAGE")
alert_manager.clear("DISK_HIGH")
elif disk>=DISK_HIGH:
alert_manager.request("DISK_HIGH",1,"DISK SPACE LOW",f"USED {disk:.1f}%","FREE < 10%","CHECK STORAGE")
alert_manager.clear("DISK_CRITICAL")
else:
alert_manager.clear("DISK_HIGH"); alert_manager.clear("DISK_CRITICAL")
if disk is not None:
if disk>=DISK_CRIT:
alert_manager.request("DISK_CRITICAL",2,"DISK CRITICAL",f"USED {disk:.1f}%","FREE < 5%","CHECK STORAGE")
alert_manager.clear("DISK_HIGH")
elif disk>=DISK_HIGH:
alert_manager.request("DISK_HIGH",1,"DISK SPACE LOW",f"USED {disk:.1f}%","FREE < 10%","CHECK STORAGE")
alert_manager.clear("DISK_CRITICAL")
else:
alert_manager.clear("DISK_HIGH"); alert_manager.clear("DISK_CRITICAL")
if not link_ok:
alert_manager.request("NETWORK_DOWN",2,"NETWORK DOWN","NO LAN / WIFI","NO IP ADDRESS","")
alert_manager.clear("INTERNET_DOWN")
@@ -423,11 +426,18 @@ def cleanup():
def write_status(cpu,temp,mem,disk):
display_state=alert_manager.status_snapshot()
memory_total,memory_used,memory_available,memory_calculated=memory_usage()
disk_total,disk_used,disk_free=disk_usage("/")
storage=storage_cache.sample()
disk_total,disk_used,disk_free=(storage[k] for k in ("total","used","free")) if storage else (None,None,None)
signal=wifi_percent(net_metric) if net_kind=="WIF" else None
state={
"version":APP_VERSION,"timestamp":int(time.time()),
"collection":{"interval_seconds":min(1.0,SENSOR_INTERVAL,NETWORK_INTERVAL,SERVICE_INTERVAL,HEALTH_INTERVAL)},"uptime_seconds":uptime_seconds(),
"collection":{"interval_seconds":min(1.0,SENSOR_INTERVAL,NETWORK_INTERVAL,SERVICE_INTERVAL,HEALTH_INTERVAL),
"refresh":{
"system":{"interval_seconds":SENSOR_INTERVAL,"observed_at":sensor_observed_at,"next_update_at":sensor_observed_at+SENSOR_INTERVAL if sensor_observed_at else None},
"network":{"interval_seconds":NETWORK_INTERVAL,"observed_at":network_observed_at,"next_update_at":network_observed_at+NETWORK_INTERVAL if network_observed_at else None},
"storage":{"interval_seconds":86400,"observed_at":storage_cache.observed_at,"next_update_at":storage_cache.next_update_at,"error":storage_cache.error},
"display":{"interval_seconds":1,"next_update_at":time.time()+1},
"alerts":{"interval_seconds":1,"next_update_at":time.time()+1}}},"uptime_seconds":uptime_seconds(),
"machine":{"id":DEVICE_ID,"name":DEVICE_NAME,"hostname":socket.gethostname()},
"device":DEVICE_INFO,
"system":{"observed_at":sensor_observed_at,"load_average":load_average(),"cpu_percent":round(cpu,1),
@@ -435,7 +445,8 @@ def write_status(cpu,temp,mem,disk):
"memory_percent":round(memory_calculated if memory_total else mem,1),
"memory_total_bytes":memory_total,"memory_used_bytes":memory_used,
"memory_available_bytes":memory_available,
"disk_percent":round(disk,1),"disk_total_bytes":disk_total,
"storage_observed_at":storage_cache.observed_at,
"disk_percent":round(disk,1) if disk is not None else None,"disk_total_bytes":disk_total,
"disk_used_bytes":disk_used,"disk_free_bytes":disk_free,
"root_readonly":bool(fs_readonly)},
"power":health,
+57
View File
@@ -0,0 +1,57 @@
"""Daily filesystem capacity cache, persisted in RAM across monitor restarts."""
import json
import time
from pathlib import Path
from shutil import disk_usage
class StorageCache:
def __init__(self, path, reader=disk_usage, clock=time.time):
self.path, self.reader, self.clock = Path(path), reader, clock
self.values = None
self.observed_at = None
self.next_update_at = 0
self.error = False
try:
data = json.loads(self.path.read_text())
if not isinstance(data,dict):raise ValueError('invalid cache')
retry = data.get('next_update_at')
if data.get('error') is True and isinstance(retry,(int,float)) and self.clock() < retry <= self.clock()+3600:
self.error, self.next_update_at = True, retry
values = data['values']
if all(isinstance(values[k], int) and not isinstance(values[k], bool) and values[k] >= 0 for k in ('total', 'used', 'free')) and values['total'] > 0 and values['used'] <= values['total']:
at = data['observed_at']
if isinstance(at, (int, float)) and 0 <= self.clock() - at < 90000:
self.values, self.observed_at = values, at
if not self.error:self.next_update_at = at + 86400
except (OSError, ValueError, KeyError, TypeError):
pass
def sample(self):
now = self.clock()
if now < self.next_update_at:
return self.values
try:
usage = self.reader('/')
if usage.total <= 0 or not 0 <= usage.used <= usage.total or usage.free < 0:
raise ValueError('invalid capacity')
except (OSError, ValueError):
self.error = True
self.next_update_at = now + 3600
self.persist()
return self.values
self.values = dict(total=usage.total, used=usage.used, free=usage.free)
self.observed_at = now
self.next_update_at = now + 86400
self.error = False
self.persist()
return self.values
def persist(self):
try:
self.path.parent.mkdir(parents=True, exist_ok=True)
self.path.write_text(json.dumps(dict(values=self.values, observed_at=self.observed_at,
next_update_at=self.next_update_at, error=self.error)))
except OSError:
# The in-memory sample remains valid even if the RAM cache cannot be persisted.
pass