- Deploy Music Assistant on Pi (10.0.0.199:8095) with host networking for Chromecast mDNS discovery, Spotify + SMB library support - Switch primary LLM from Ollama to Claude Sonnet 4 (Anthropic API), local models remain as fallback - Add model info tag under each assistant message in dashboard chat, persisted in conversation JSON - Rewrite homeai-agent/setup.sh: loads .env, injects API keys into plists, symlinks plists to ~/Library/LaunchAgents/, smoke tests services - Update install_service() in common.sh to use symlinks instead of copies - Open UFW ports on Pi for Music Assistant (8095, 8097, 8927) - Add ANTHROPIC_API_KEY to openclaw + bridge launchd plists Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
91 lines
2.4 KiB
Python
Executable File
91 lines
2.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
HomeAI Reminder Daemon — checks ~/homeai-data/reminders.json every 60s
|
|
and fires TTS via POST http://localhost:8081/api/tts when reminders are due.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import time
|
|
import urllib.request
|
|
from datetime import datetime
|
|
|
|
REMINDERS_FILE = os.path.expanduser("~/homeai-data/reminders.json")
|
|
TTS_URL = "http://localhost:8081/api/tts"
|
|
CHECK_INTERVAL = 60 # seconds
|
|
|
|
|
|
def load_reminders():
|
|
try:
|
|
with open(REMINDERS_FILE) as f:
|
|
return json.load(f)
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
return {"reminders": []}
|
|
|
|
|
|
def save_reminders(data):
|
|
with open(REMINDERS_FILE, "w") as f:
|
|
json.dump(data, f, indent=2)
|
|
|
|
|
|
def fire_tts(message):
|
|
"""Speak reminder via the OpenClaw bridge TTS endpoint."""
|
|
try:
|
|
payload = json.dumps({"text": f"Reminder: {message}"}).encode()
|
|
req = urllib.request.Request(
|
|
TTS_URL,
|
|
data=payload,
|
|
headers={"Content-Type": "application/json"},
|
|
method="POST"
|
|
)
|
|
urllib.request.urlopen(req, timeout=30)
|
|
print(f"[{datetime.now().isoformat()}] TTS fired: {message}")
|
|
return True
|
|
except Exception as e:
|
|
print(f"[{datetime.now().isoformat()}] TTS error: {e}")
|
|
return False
|
|
|
|
|
|
def check_reminders():
|
|
data = load_reminders()
|
|
now = datetime.now()
|
|
changed = False
|
|
|
|
for r in data.get("reminders", []):
|
|
if r.get("fired"):
|
|
continue
|
|
|
|
try:
|
|
due = datetime.fromisoformat(r["due_at"])
|
|
except (KeyError, ValueError):
|
|
continue
|
|
|
|
if now >= due:
|
|
print(f"[{now.isoformat()}] Reminder due: {r.get('message', '?')}")
|
|
fire_tts(r["message"])
|
|
r["fired"] = True
|
|
changed = True
|
|
|
|
if changed:
|
|
# Clean up fired reminders older than 24h
|
|
cutoff = (now.timestamp() - 86400) * 1000
|
|
data["reminders"] = [
|
|
r for r in data["reminders"]
|
|
if not r.get("fired") or int(r.get("id", "0")) > cutoff
|
|
]
|
|
save_reminders(data)
|
|
|
|
|
|
def main():
|
|
print(f"[{datetime.now().isoformat()}] Reminder daemon started (check every {CHECK_INTERVAL}s)")
|
|
while True:
|
|
try:
|
|
check_reminders()
|
|
except Exception as e:
|
|
print(f"[{datetime.now().isoformat()}] Error: {e}")
|
|
time.sleep(CHECK_INTERVAL)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|