Skip to content

Commit 34da839

Browse files
jrx-codeclaude
andcommitted
feat: v0.15.0 — scheduled periodic scans of installed HACS components
- New scheduler module: asyncio background task for periodic re-scans - Configurable interval (default 24h), enable/disable via API - Settings persistence: schedule_enabled, schedule_interval_hours - API: GET/POST /api/scheduler (status + enable/disable) - Auto-start on boot if previously enabled - Scheduler status visible in /api/system Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 399bc66 commit 34da839

4 files changed

Lines changed: 131 additions & 1 deletion

File tree

ha-sandbox/app/main.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from app.report.mqtt import disconnect, publish_discovery, publish_status
2020
from app.scanner.hacs_list import fetch_installed_hacs, repo_to_url, test_ha_connection
2121
from app.scanner.pipeline import run_scan
22+
from app import scheduler
2223

2324
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s")
2425
log = logging.getLogger(__name__)
@@ -49,7 +50,12 @@ async def lifespan(app: FastAPI):
4950
publish_status("idle")
5051
except Exception as e:
5152
log.warning("MQTT init failed (non-fatal): %s", e)
53+
# Start scheduled scans if enabled
54+
cfg = app_settings.load()
55+
if cfg.get("schedule_enabled"):
56+
scheduler.start(cfg.get("schedule_interval_hours", 24))
5257
yield
58+
scheduler.stop()
5359
storage.close()
5460
disconnect()
5561

@@ -386,6 +392,33 @@ async def api_reputation_all():
386392
return JSONResponse(content=get_all_reputations(conn))
387393

388394

395+
# --- Scheduler API ---
396+
397+
@app.get("/api/scheduler")
398+
async def api_scheduler_status():
399+
return JSONResponse(content=scheduler.status())
400+
401+
402+
@app.post("/api/scheduler")
403+
async def api_scheduler_update(request: Request):
404+
"""Enable/disable scheduled scans.
405+
406+
Body: {"enabled": true/false, "interval_hours": 24}
407+
"""
408+
data = await request.json()
409+
enabled = data.get("enabled", False)
410+
interval = float(data.get("interval_hours", 24))
411+
412+
app_settings.save({"schedule_enabled": enabled, "schedule_interval_hours": interval})
413+
414+
if enabled and interval > 0:
415+
scheduler.start(interval)
416+
else:
417+
scheduler.stop()
418+
419+
return JSONResponse(content={"ok": True, **scheduler.status()})
420+
421+
389422
@app.get("/api/system")
390423
async def api_system_info():
391424
repos_dir = Path(app_settings.get("repos_dir", "/data/repos"))
@@ -398,4 +431,5 @@ async def api_system_info():
398431
"reports": report_count,
399432
"repos_cached": repo_count,
400433
"cache_size_mb": round(repo_size / 1024 / 1024, 1),
434+
"scheduler": scheduler.status(),
401435
})

ha-sandbox/app/scheduler.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""Scheduled periodic scans of installed HACS components."""
2+
3+
import asyncio
4+
import logging
5+
from datetime import datetime
6+
7+
log = logging.getLogger(__name__)
8+
9+
_task: asyncio.Task | None = None
10+
_interval_hours: float = 0
11+
_enabled: bool = False
12+
13+
14+
async def _scheduled_loop():
15+
"""Run periodic scan of installed HACS components."""
16+
from app.scanner.hacs_list import fetch_installed_hacs, repo_to_url
17+
from app.scanner.pipeline import run_scan
18+
from app import storage
19+
from app.report.mqtt import publish_status
20+
21+
while True:
22+
await asyncio.sleep(_interval_hours * 3600)
23+
if not _enabled:
24+
continue
25+
26+
log.info("Scheduled scan starting (interval=%gh)", _interval_hours)
27+
publish_status("scheduled_scan")
28+
29+
try:
30+
installed = await fetch_installed_hacs()
31+
if not installed:
32+
log.warning("Scheduled scan: no HACS components found")
33+
continue
34+
35+
scanned = 0
36+
for comp in installed:
37+
url = repo_to_url(comp.get("full_name", ""))
38+
if not url:
39+
continue
40+
name = comp.get("name", comp.get("full_name", ""))
41+
job_id = f"sched:{name}"
42+
storage.create_job(job_id, name, url, batch_id="scheduled")
43+
try:
44+
await run_scan(url, name)
45+
storage.complete_job(job_id)
46+
scanned += 1
47+
except Exception as e:
48+
storage.fail_job(job_id, str(e))
49+
log.warning("Scheduled scan failed for %s: %s", name, e)
50+
51+
log.info("Scheduled scan done: %d/%d components scanned", scanned, len(installed))
52+
publish_status("idle")
53+
storage.cleanup_repo_cache()
54+
55+
except Exception as e:
56+
log.exception("Scheduled scan loop error: %s", e)
57+
publish_status("idle")
58+
59+
60+
def start(interval_hours: float = 24):
61+
"""Start the scheduled scan loop."""
62+
global _task, _interval_hours, _enabled
63+
if interval_hours <= 0:
64+
log.info("Scheduled scans disabled (interval=0)")
65+
return
66+
67+
_interval_hours = interval_hours
68+
_enabled = True
69+
70+
if _task and not _task.done():
71+
log.info("Scheduler already running, updating interval to %gh", interval_hours)
72+
return
73+
74+
_task = asyncio.create_task(_scheduled_loop())
75+
log.info("Scheduled scans enabled: every %gh", interval_hours)
76+
77+
78+
def stop():
79+
"""Stop the scheduled scan loop."""
80+
global _task, _enabled
81+
_enabled = False
82+
if _task and not _task.done():
83+
_task.cancel()
84+
_task = None
85+
log.info("Scheduled scans disabled")
86+
87+
88+
def status() -> dict:
89+
"""Return scheduler status."""
90+
return {
91+
"enabled": _enabled,
92+
"interval_hours": _interval_hours,
93+
"running": _task is not None and not _task.done() if _task else False,
94+
}

ha-sandbox/app/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
"ai_timeout": 300,
3131
"max_file_size_kb": 500,
3232
"log_level": "info",
33+
"schedule_enabled": False,
34+
"schedule_interval_hours": 24,
3335
}
3436

3537
# Public API provider presets

ha-sandbox/config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: "HA Security Sandbox"
2-
version: "0.14.0"
2+
version: "0.15.0"
33
slug: ha_security_sandbox
44
description: "Security scanner for Home Assistant custom components — static analysis + AI review"
55
url: "https://github.com/jrx-code/ha-security-sandbox"

0 commit comments

Comments
 (0)