feat: Music Assistant, Claude primary LLM, model tag in chat, setup.sh rewrite

- 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>
This commit is contained in:
Aodhan Collins
2026-03-18 22:21:28 +00:00
parent 60eb89ea42
commit 117254d560
17 changed files with 1399 additions and 361 deletions

90
homeai-agent/reminder-daemon.py Executable file
View File

@@ -0,0 +1,90 @@
#!/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()