Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
174ac747c5 | ||
|
|
b63fa39f32 | ||
|
|
e828fd11ee |
@@ -59,3 +59,13 @@ This project targets Raspberry Pi. Other Linux platforms are not covered by its
|
|||||||
## v3.8.0 网络流量
|
## v3.8.0 网络流量
|
||||||
|
|
||||||
主接口每个网络采集周期读取 sysfs 的 rx_bytes/tx_bytes,以单调时间差计算字节/秒,API 新增 rx_bytes_per_second 和 tx_bytes_per_second。首次采样、接口切换、计数器重置或读取失败返回空值;不修改监控告警、硬件插件控制或 API 可选安装行为。2026-10-14 已在 Gateway 部署,实机上下行速率与 Console 标准历史指标验证通过。
|
主接口每个网络采集周期读取 sysfs 的 rx_bytes/tx_bytes,以单调时间差计算字节/秒,API 新增 rx_bytes_per_second 和 tx_bytes_per_second。首次采样、接口切换、计数器重置或读取失败返回空值;不修改监控告警、硬件插件控制或 API 可选安装行为。2026-10-14 已在 Gateway 部署,实机上下行速率与 Console 标准历史指标验证通过。
|
||||||
|
|
||||||
|
## v3.9.0 采样时间
|
||||||
|
|
||||||
|
API 状态新增 system.observed_at、network.observed_at 和 collection.interval_seconds。Console 可每秒读取缓存,网络等低频指标保留实际采样时间,供历史去重。数据来源与原网络采样周期保持。
|
||||||
|
|
||||||
|
生产 v3.9.0 与按设备周期采集的核验见 [部署记录](docs/DEPLOYMENT-v3.9.0-2026-10-15.md)。
|
||||||
|
|
||||||
|
## v3.10.0 独立采集周期
|
||||||
|
|
||||||
|
实时状态提供分组采样时间与下一次更新计划。容量每天读取一次,RAM 缓存跨服务重启恢复;读取失败延后一小时重试,避免反复查询存储。网络周期可独立配置,生产计划调整为 1 秒。
|
||||||
|
|||||||
+6
-3
@@ -1,17 +1,20 @@
|
|||||||
ecf1884bb89b86ed4573d4d922a971abde43772bf7b2ffa6a999a12fbfb72caf .gitignore
|
ecf1884bb89b86ed4573d4d922a971abde43772bf7b2ffa6a999a12fbfb72caf .gitignore
|
||||||
56b7a3ed6a6a8dbbcf02a80b6219a5e6aab0df2ef04cdff90895784c311e5cfa README.md
|
948e700459c05a045234edfbbdd1efa240f9a0350221a40029b7416c0587e36c README.md
|
||||||
64a9708701f8c8d872a587e8a2845afff463797b06a2791319daadd705933d31 README.zh-CN.md
|
64a9708701f8c8d872a587e8a2845afff463797b06a2791319daadd705933d31 README.zh-CN.md
|
||||||
c5853f283c0951b43f2bacbc49e66c26d3a605a184d427828e042eb9236d2740 app/network_rates.py
|
c5853f283c0951b43f2bacbc49e66c26d3a605a184d427828e042eb9236d2740 app/network_rates.py
|
||||||
003e6766552823e8cd8ec56888c385d0607df0c14ab7dff166472fcf6914ff9c app/pigway_pi_control.py
|
69877b4ad2b8307db69495d629988ff4bca838dea9e36d839165029fdb6ed4a8 app/pigway_pi_control.py
|
||||||
757eb6a83ca07740a2389af9b51c6fd5d940b4eb86abbc603f231409416d45db app/pigway_pi_control_api.py
|
757eb6a83ca07740a2389af9b51c6fd5d940b4eb86abbc603f231409416d45db app/pigway_pi_control_api.py
|
||||||
3a8a368bbf299eb2d6cccc968a4aacf070058bddebdc5176083a97ac579d720d app/plugin_api.py
|
3a8a368bbf299eb2d6cccc968a4aacf070058bddebdc5176083a97ac579d720d app/plugin_api.py
|
||||||
|
81ca44b8bdc384c7f27a39da8e785af2450241c65200edee61db95f48d67f058 app/storage_cache.py
|
||||||
d5a783b64a10fa43191a813ef73ef8f762e359b0bd42ec155724fa46216298b0 config/pigway-pi-control.conf
|
d5a783b64a10fa43191a813ef73ef8f762e359b0bd42ec155724fa46216298b0 config/pigway-pi-control.conf
|
||||||
9d45f22221012a7fdb3406ce74a6113f618e9930fe460efb3869efd099e3b1d8 docs/DEPLOYMENT-v3.8.0-2026-10-14.md
|
9d45f22221012a7fdb3406ce74a6113f618e9930fe460efb3869efd099e3b1d8 docs/DEPLOYMENT-v3.8.0-2026-10-14.md
|
||||||
|
2a3cd7f58c46b7f864affd7ec64fbec38ea2d6306160cd8d4f0075a779251b5f docs/DEPLOYMENT-v3.9.0-2026-10-15.md
|
||||||
e3bf3e7070ea62c0526791bed922688c3fb49fee36ca9c375d080119968feb72 docs/HARDWARE_PLUGIN_API.md
|
e3bf3e7070ea62c0526791bed922688c3fb49fee36ca9c375d080119968feb72 docs/HARDWARE_PLUGIN_API.md
|
||||||
b54ec1f3e9fb15473910686c54142a85d387e44ea5df427a4cf1b06052e789b3 docs/RASPBERRY_PI_OS_TRIXIE_FRESH_INSTALL.md
|
b54ec1f3e9fb15473910686c54142a85d387e44ea5df427a4cf1b06052e789b3 docs/RASPBERRY_PI_OS_TRIXIE_FRESH_INSTALL.md
|
||||||
9222a2b6a38f6b6d7d88b30c05413c68c05f69ee499c8479eca05ee7c62e87c3 docs/RASPBERRY_PI_OS_TRIXIE_FRESH_INSTALL.zh-CN.md
|
9222a2b6a38f6b6d7d88b30c05413c68c05f69ee499c8479eca05ee7c62e87c3 docs/RASPBERRY_PI_OS_TRIXIE_FRESH_INSTALL.zh-CN.md
|
||||||
c104443a4c951ea7916f74473510362f684df7d916bf2a436b779513ba3fe396 install.sh
|
1b0aeaf4997bdb02b967d538cf276ad7462a3e22ec35fc0d046d779ef56cd1f8 install.sh
|
||||||
8a593c290da5bd0d037aee86c35264512afd574a2a0f60f2c29e9db63e0109bd systemd/pigway-pi-control-api.service
|
8a593c290da5bd0d037aee86c35264512afd574a2a0f60f2c29e9db63e0109bd systemd/pigway-pi-control-api.service
|
||||||
1cb55de6b104a233066495e9786319305fa4f1b6f25483c108ab4f5ac9aa5700 systemd/pigway-pi-control.service
|
1cb55de6b104a233066495e9786319305fa4f1b6f25483c108ab4f5ac9aa5700 systemd/pigway-pi-control.service
|
||||||
8b97229abfb31c68464eb5754bc3a8fec7a6a78e216d74a86b7cc89486700740 tests/test_network_rates.py
|
8b97229abfb31c68464eb5754bc3a8fec7a6a78e216d74a86b7cc89486700740 tests/test_network_rates.py
|
||||||
|
f78e31e0eeee4e02532885ace5c8f6f7011d084e4c7440b704408bc88495e40e tests/test_storage_cache.py
|
||||||
9f59e7313ee58e687ae6dd29d175c67082674254ac5c5f54168b2735b64c20a4 uninstall.sh
|
9f59e7313ee58e687ae6dd29d175c67082674254ac5c5f54168b2735b64c20a4 uninstall.sh
|
||||||
|
|||||||
+32
-17
@@ -1,12 +1,13 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import time,socket,subprocess,configparser,signal,re,argparse,json,os,platform
|
import time,socket,subprocess,configparser,signal,re,argparse,json,os,platform
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
APP_VERSION="3.8.0"
|
APP_VERSION="3.10.0"
|
||||||
import sys
|
import sys
|
||||||
sys.path.append("/usr/local/share/pigway-pi-control")
|
sys.path.append("/usr/local/share/pigway-pi-control")
|
||||||
from shutil import disk_usage
|
|
||||||
from plugin_api import Publisher,registered,discover
|
from plugin_api import Publisher,registered,discover
|
||||||
from network_rates import NetworkRates
|
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"
|
CFG="/etc/pigway-pi-control.conf"
|
||||||
STATUS_PATH="/run/pigway-pi-control/status.json"
|
STATUS_PATH="/run/pigway-pi-control/status.json"
|
||||||
@@ -105,6 +106,8 @@ prev_cpu=None
|
|||||||
page=0; page_since=time.monotonic()
|
page=0; page_since=time.monotonic()
|
||||||
cpu_hi=mem_hi=0
|
cpu_hi=mem_hi=0
|
||||||
net_ok=link_ok=True
|
net_ok=link_ok=True
|
||||||
|
sensor_observed_at=None
|
||||||
|
network_observed_at=None
|
||||||
network_rates=NetworkRates()
|
network_rates=NetworkRates()
|
||||||
net_rx_bytes_per_second=None; net_tx_bytes_per_second=None
|
net_rx_bytes_per_second=None; net_tx_bytes_per_second=None
|
||||||
net_iface=""; net_kind="NET"; net_metric="DOWN"; net_speed_mbps=None; ip_label="IP4"; ip="NO IP"
|
net_iface=""; net_kind="NET"; net_metric="DOWN"; net_speed_mbps=None; ip_label="IP4"; ip="NO IP"
|
||||||
@@ -142,7 +145,8 @@ def mem_pct():
|
|||||||
k,v=l.split(":",1); vals[k]=int(v.split()[0])
|
k,v=l.split(":",1); vals[k]=int(v.split()[0])
|
||||||
return 100*(vals["MemTotal"]-vals["MemAvailable"])/vals["MemTotal"]
|
return 100*(vals["MemTotal"]-vals["MemAvailable"])/vals["MemTotal"]
|
||||||
def disk_pct():
|
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):
|
def _run(args,timeout=2):
|
||||||
try: return subprocess.check_output(args,text=True,stderr=subprocess.DEVNULL,timeout=timeout).strip()
|
try: return subprocess.check_output(args,text=True,stderr=subprocess.DEVNULL,timeout=timeout).strip()
|
||||||
except Exception: return ""
|
except Exception: return ""
|
||||||
@@ -324,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")
|
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","")
|
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")
|
else: alert_manager.clear("MEMORY_HIGH")
|
||||||
if disk>=DISK_CRIT:
|
if disk is not None:
|
||||||
alert_manager.request("DISK_CRITICAL",2,"DISK CRITICAL",f"USED {disk:.1f}%","FREE < 5%","CHECK STORAGE")
|
if disk>=DISK_CRIT:
|
||||||
alert_manager.clear("DISK_HIGH")
|
alert_manager.request("DISK_CRITICAL",2,"DISK CRITICAL",f"USED {disk:.1f}%","FREE < 5%","CHECK STORAGE")
|
||||||
elif disk>=DISK_HIGH:
|
alert_manager.clear("DISK_HIGH")
|
||||||
alert_manager.request("DISK_HIGH",1,"DISK SPACE LOW",f"USED {disk:.1f}%","FREE < 10%","CHECK STORAGE")
|
elif disk>=DISK_HIGH:
|
||||||
alert_manager.clear("DISK_CRITICAL")
|
alert_manager.request("DISK_HIGH",1,"DISK SPACE LOW",f"USED {disk:.1f}%","FREE < 10%","CHECK STORAGE")
|
||||||
else:
|
alert_manager.clear("DISK_CRITICAL")
|
||||||
alert_manager.clear("DISK_HIGH"); alert_manager.clear("DISK_CRITICAL")
|
else:
|
||||||
|
alert_manager.clear("DISK_HIGH"); alert_manager.clear("DISK_CRITICAL")
|
||||||
if not link_ok:
|
if not link_ok:
|
||||||
alert_manager.request("NETWORK_DOWN",2,"NETWORK DOWN","NO LAN / WIFI","NO IP ADDRESS","")
|
alert_manager.request("NETWORK_DOWN",2,"NETWORK DOWN","NO LAN / WIFI","NO IP ADDRESS","")
|
||||||
alert_manager.clear("INTERNET_DOWN")
|
alert_manager.clear("INTERNET_DOWN")
|
||||||
@@ -421,22 +426,31 @@ def cleanup():
|
|||||||
def write_status(cpu,temp,mem,disk):
|
def write_status(cpu,temp,mem,disk):
|
||||||
display_state=alert_manager.status_snapshot()
|
display_state=alert_manager.status_snapshot()
|
||||||
memory_total,memory_used,memory_available,memory_calculated=memory_usage()
|
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
|
signal=wifi_percent(net_metric) if net_kind=="WIF" else None
|
||||||
state={
|
state={
|
||||||
"version":APP_VERSION,"timestamp":int(time.time()),"uptime_seconds":uptime_seconds(),
|
"version":APP_VERSION,"timestamp":int(time.time()),
|
||||||
|
"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()},
|
"machine":{"id":DEVICE_ID,"name":DEVICE_NAME,"hostname":socket.gethostname()},
|
||||||
"device":DEVICE_INFO,
|
"device":DEVICE_INFO,
|
||||||
"system":{"load_average":load_average(),"cpu_percent":round(cpu,1),
|
"system":{"observed_at":sensor_observed_at,"load_average":load_average(),"cpu_percent":round(cpu,1),
|
||||||
"cpu_freq_mhz":round(cpu_freq_mhz()),"temperature_c":round(temp,1),
|
"cpu_freq_mhz":round(cpu_freq_mhz()),"temperature_c":round(temp,1),
|
||||||
"memory_percent":round(memory_calculated if memory_total else mem,1),
|
"memory_percent":round(memory_calculated if memory_total else mem,1),
|
||||||
"memory_total_bytes":memory_total,"memory_used_bytes":memory_used,
|
"memory_total_bytes":memory_total,"memory_used_bytes":memory_used,
|
||||||
"memory_available_bytes":memory_available,
|
"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,
|
"disk_used_bytes":disk_used,"disk_free_bytes":disk_free,
|
||||||
"root_readonly":bool(fs_readonly)},
|
"root_readonly":bool(fs_readonly)},
|
||||||
"power":health,
|
"power":health,
|
||||||
"network":{"interface":net_iface,"kind":net_kind,"metric":net_metric,
|
"network":{"observed_at":network_observed_at,"interface":net_iface,"kind":net_kind,"metric":net_metric,
|
||||||
"link_speed_mbps":net_speed_mbps,
|
"link_speed_mbps":net_speed_mbps,
|
||||||
"rx_bytes_per_second":net_rx_bytes_per_second,
|
"rx_bytes_per_second":net_rx_bytes_per_second,
|
||||||
"tx_bytes_per_second":net_tx_bytes_per_second,
|
"tx_bytes_per_second":net_tx_bytes_per_second,
|
||||||
@@ -484,12 +498,13 @@ try:
|
|||||||
if now-last_sensor >= SENSOR_INTERVAL:
|
if now-last_sensor >= SENSOR_INTERVAL:
|
||||||
sample_elapsed=now-last_sensor if last_sensor else 0.0
|
sample_elapsed=now-last_sensor if last_sensor else 0.0
|
||||||
c=cpu_pct(); t=temp_c(); m=mem_pct(); d=disk_pct()
|
c=cpu_pct(); t=temp_c(); m=mem_pct(); d=disk_pct()
|
||||||
latest=(c,t,m,d); last_sensor=now; agent["last_sensor_ok"]=now
|
latest=(c,t,m,d); sensor_observed_at=int(time.time()); last_sensor=now; agent["last_sensor_ok"]=now
|
||||||
|
|
||||||
if now>=monitor_ready_at and now-last_net >= NETWORK_INTERVAL:
|
if now>=monitor_ready_at and now-last_net >= NETWORK_INTERVAL:
|
||||||
old_id=(net_iface,ip)
|
old_id=(net_iface,ip)
|
||||||
net_iface,net_kind,net_metric,net_speed_mbps,ip_label,ip=primary_network()
|
net_iface,net_kind,net_metric,net_speed_mbps,ip_label,ip=primary_network()
|
||||||
net_rx_bytes_per_second,net_tx_bytes_per_second=network_rates.sample(net_iface,now)
|
net_rx_bytes_per_second,net_tx_bytes_per_second=network_rates.sample(net_iface,now)
|
||||||
|
network_observed_at=int(time.time())
|
||||||
raw_link_ok=ip!="NO IP"; raw_net_ok=internet() if raw_link_ok else False
|
raw_link_ok=ip!="NO IP"; raw_net_ok=internet() if raw_link_ok else False
|
||||||
if raw_link_ok:
|
if raw_link_ok:
|
||||||
link_failure_since=None;link_ok=True
|
link_failure_since=None;link_ok=True
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# pigway-raspberry-pi-agent v3.9.0
|
||||||
|
|
||||||
|
2026-10-15 已与 Console v2.4.0 部署。API 状态提供 collection.interval_seconds、system.observed_at 和 network.observed_at。Gateway 实际版本 3.9.0,状态采样时间与 1 秒/5 秒更新实测通过。旧程序在 /home/way/pigway-agent-backup-v3.9.0-20261015,目录 0700。
|
||||||
|
|
||||||
|
生产两设备在线。认证 API 连续观测确认树莓派缓存每秒、网络 5 秒、NAS 实时约 10 秒;最近三个网络 10 秒历史桶各 2 个样本,没有每秒重复入库。Console 保留原名称、证书、存储缓存和一年历史策略。部署详情见 Console [生产记录](https://tea.pigway.com/way/pigway-device-console/src/branch/main/docs/DEPLOYMENT-v2.4.0-2026-10-15.md)。
|
||||||
+2
-1
@@ -12,13 +12,14 @@ for arg in "$@"; do
|
|||||||
*) echo "ERROR: unknown option: $arg (hardware is installed separately)" >&2; exit 2 ;;
|
*) echo "ERROR: unknown option: $arg (hardware is installed separately)" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
for required in app/pigway_pi_control.py app/plugin_api.py app/network_rates.py app/pigway_pi_control_api.py config/pigway-pi-control.conf systemd/pigway-pi-control.service systemd/pigway-pi-control-api.service; do
|
for required in app/pigway_pi_control.py app/plugin_api.py app/network_rates.py app/storage_cache.py app/pigway_pi_control_api.py config/pigway-pi-control.conf systemd/pigway-pi-control.service systemd/pigway-pi-control-api.service; do
|
||||||
[ -f "$required" ] || { echo "ERROR: required file missing: $required" >&2; exit 1; }
|
[ -f "$required" ] || { echo "ERROR: required file missing: $required" >&2; exit 1; }
|
||||||
done
|
done
|
||||||
[ "$(id -u)" -eq 0 ] || { echo 'ERROR: run installer as root' >&2; exit 1; }
|
[ "$(id -u)" -eq 0 ] || { echo 'ERROR: run installer as root' >&2; exit 1; }
|
||||||
# The monitor has no I2C, GPIO, Pillow, smbus2 or cooling-board requirement.
|
# The monitor has no I2C, GPIO, Pillow, smbus2 or cooling-board requirement.
|
||||||
command -v python3 >/dev/null || { apt-get update; apt-get install -y python3; }
|
command -v python3 >/dev/null || { apt-get update; apt-get install -y python3; }
|
||||||
mkdir -p /usr/local/share/pigway-pi-control
|
mkdir -p /usr/local/share/pigway-pi-control
|
||||||
|
install -m 0644 app/storage_cache.py /usr/local/share/pigway-pi-control/storage_cache.py
|
||||||
install -m 0644 app/network_rates.py /usr/local/share/pigway-pi-control/network_rates.py
|
install -m 0644 app/network_rates.py /usr/local/share/pigway-pi-control/network_rates.py
|
||||||
install -m 0644 app/plugin_api.py /usr/local/share/pigway-pi-control/plugin_api.py
|
install -m 0644 app/plugin_api.py /usr/local/share/pigway-pi-control/plugin_api.py
|
||||||
install -m 0755 app/pigway_pi_control.py /usr/local/sbin/pigway-pi-control
|
install -m 0755 app/pigway_pi_control.py /usr/local/sbin/pigway-pi-control
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import sys,tempfile,unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from types import SimpleNamespace
|
||||||
|
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'app'))
|
||||||
|
from storage_cache import StorageCache
|
||||||
|
|
||||||
|
|
||||||
|
class StorageTests(unittest.TestCase):
|
||||||
|
def test_daily_reads_and_restart_cache(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
now=[1000];reads=[]
|
||||||
|
def reader(path):
|
||||||
|
reads.append(path);return SimpleNamespace(total=1000,used=200,free=800)
|
||||||
|
p=Path(directory)/'cache.json'
|
||||||
|
cache=StorageCache(p,reader,lambda:now[0])
|
||||||
|
self.assertEqual(cache.sample()['used'],200)
|
||||||
|
for i in range(20):now[0]+=1;cache.sample()
|
||||||
|
self.assertEqual(len(reads),1)
|
||||||
|
restored=StorageCache(p,reader,lambda:now[0]);restored.sample()
|
||||||
|
self.assertEqual(len(reads),1)
|
||||||
|
now[0]=87400;restored.sample()
|
||||||
|
self.assertEqual(len(reads),2)
|
||||||
|
|
||||||
|
def test_failure_does_not_fabricate_zero_or_retry_each_second(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
now=[1000];reads=[]
|
||||||
|
def fail(path):reads.append(path);raise OSError('unavailable')
|
||||||
|
cache=StorageCache(Path(directory)/'cache.json',fail,lambda:now[0])
|
||||||
|
self.assertIsNone(cache.sample());self.assertTrue(cache.error)
|
||||||
|
now[0]+=5;self.assertIsNone(cache.sample())
|
||||||
|
self.assertEqual(len(reads),1)
|
||||||
|
|
||||||
|
def test_failed_read_cooldown_survives_restart(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
reads=[]
|
||||||
|
def fail(path):reads.append(path);raise OSError('unavailable')
|
||||||
|
p=Path(directory)/'cache.json'
|
||||||
|
StorageCache(p,fail,lambda:1000).sample()
|
||||||
|
restored=StorageCache(p,fail,lambda:1005)
|
||||||
|
self.assertTrue(restored.error);self.assertIsNone(restored.sample())
|
||||||
|
self.assertEqual(len(reads),1)
|
||||||
Reference in New Issue
Block a user