Bump to v2.2.0: fix warning/bubble sounds, add klaxon, refresh docs

- index.html: fix playWarningSound (440→550 Hz, louder), fix playBubbleSound
  (audible volumes/durations), add looping klaxon sound (sawtooth wah-wah),
  stopKlaxon() on tap and state clear, bump VERSION to v2.2.0
- kao_tui.py: add klaxon to SOUNDS list, drop notify duration 5→2s for
  faster iteration; also include improved post() error reporting
- CLAUDE.md: add kao_tui.py to file structure, fix personality table
  (remove ˙▿˙ row not in aggregator), add klaxon to sound list
- README.md: add klaxon to sound list, update counts
- openapi.yaml: bump version to 2.2.0, add klaxon to sound enum

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 17:30:19 -06:00
parent a36fd7037a
commit 50e34b24c6
5 changed files with 53 additions and 31 deletions

View File

@@ -103,7 +103,7 @@ All detectors support: `AGGREGATOR_URL`, `CHECK_INTERVAL`, `THRESHOLD_WARNING`,
| `emote` | No | Custom emote to display |
| `color` | No | Custom color (hex, e.g., `#FF9900`) |
| `animation` | No | One of: `breathing`, `shaking`, `popping`, `celebrating`, `floating`, `bouncing`, `swaying` |
| `sound` | No | One of: `chime`, `alert`, `warning`, `critical`, `success`, `notify`, `doorbell`, `knock`, `ding`, `blip`, `siren`, `tada`, `ping`, `bubble`, `fanfare`, `alarm`, `none` |
| `sound` | No | One of: `chime`, `alert`, `warning`, `critical`, `success`, `notify`, `doorbell`, `knock`, `ding`, `blip`, `siren`, `tada`, `ping`, `bubble`, `fanfare`, `alarm`, `klaxon`, `none` |
## Priority System
@@ -123,7 +123,6 @@ The optimal state face is set once per day on `/wake` (random pick). Each mornin
| Emote | Animation | Vibe |
|-------|-----------|------|
| `( ^_^)` | breathing | calm |
| `( ˙▿˙)` | floating | content |
| `(◕‿◕)` | bouncing | cheerful |
| `( ・ω・)` | swaying | curious |
| `( ˘▽˘)` | breathing | cozy |
@@ -177,6 +176,7 @@ Use in automations:
├── kao.py # Unified entry point
├── aggregator.py # Event broker/API server
├── index.html # OLED-optimized frontend
├── kao_tui.py # Developer TUI for testing sounds/events
├── config.json # Runtime configuration
├── openapi.yaml # API documentation (OpenAPI 3.0)
├── detectors/

View File

@@ -194,7 +194,7 @@ automation:
- `emote`: Any ASCII emote (e.g., `( °o°)`, `\\(^o^)/`)
- `color`: Hex color (e.g., `#FF9900`)
- `animation`: `breathing`, `shaking`, `popping`, `celebrating`, `floating`, `bouncing`, `swaying`
- `sound`: `chime`, `alert`, `warning`, `critical`, `success`, `notify`, `doorbell`, `knock`, `ding`, `blip`, `siren`, `tada`, `ping`, `bubble`, `fanfare`, `alarm`, `none`
- `sound`: `chime`, `alert`, `warning`, `critical`, `success`, `notify`, `doorbell`, `knock`, `ding`, `blip`, `siren`, `tada`, `ping`, `bubble`, `fanfare`, `alarm`, `klaxon`, `none`
## Developer TUI
@@ -206,7 +206,7 @@ python kao_tui.py http://192.168.1.x:5100 # custom URL
```
Four tabs:
- **Sounds** — fire any of the 17 sounds via `/notify`
- **Sounds** — fire any of the 18 sounds via `/notify`
- **Faces** — send any preset emote/animation combo via `/notify`
- **Events** — post Critical/Warning/Notify events (10s TTL) or clear all
- **Controls** — Sleep, Wake, Clear all
@@ -245,7 +245,7 @@ The emote has personality! In optimal state it:
- Critical: urgent descending tone
- Notify: gentle ping
- Recovery: happy ascending chirp
- 10 additional synthesized sounds for `/notify`: `doorbell`, `knock`, `ding`, `blip`, `siren`, `tada`, `ping`, `bubble`, `fanfare`, `alarm` (alarm loops until tapped)
- 11 additional synthesized sounds for `/notify`: `doorbell`, `knock`, `ding`, `blip`, `siren`, `tada`, `ping`, `bubble`, `fanfare`, `alarm`, `klaxon` (alarm and klaxon loop until tapped)
## License

View File

@@ -217,7 +217,7 @@
const emoteEl = document.getElementById("emote");
const messageEl = document.getElementById("message");
const POLL_INTERVAL = 2000;
const VERSION = "v2.1.0";
const VERSION = "v2.2.0";
// Sound system
let audioCtx = null;
@@ -254,9 +254,9 @@
}
function playWarningSound() {
// Soft double-beep
playTone(440, 0.15);
setTimeout(() => playTone(440, 0.15), 180);
// Two-tone warning beep - differentiated pitches
playTone(440, 0.2, "sine", 0.18);
setTimeout(() => playTone(550, 0.2, "sine", 0.18), 220);
}
function playCriticalSound() {
@@ -334,8 +334,8 @@
playTone(1047, 0.15, "sine", 0.1);
}
function playBubbleSound() {
playTone(400, 0.06, "sine", 0.06);
setTimeout(() => playTone(600, 0.04, "sine", 0.04), 40);
playTone(400, 0.15, "sine", 0.10);
setTimeout(() => playTone(650, 0.12, "sine", 0.08), 100);
}
function playFanfareSound() {
playTone(523, 0.1, "sine", 0.1);
@@ -364,9 +364,28 @@
}
}
// Klaxon — looping wah-wah horn, loops until manually stopped
let klaxonInterval = null;
function playKlaxonTick() {
playTone(500, 0.22, "sawtooth", 0.22);
setTimeout(() => playTone(380, 0.22, "sawtooth", 0.22), 230);
}
function startKlaxon() {
if (klaxonInterval) return;
playKlaxonTick();
klaxonInterval = setInterval(playKlaxonTick, 470);
}
function stopKlaxon() {
if (klaxonInterval) {
clearInterval(klaxonInterval);
klaxonInterval = null;
}
}
// Play sound by name
function playSoundByName(name) {
if (name === "alarm") { startAlarm(); return; }
if (name === "klaxon") { startKlaxon(); return; }
const sounds = {
chime: playChimeSound,
alert: playAlertSound,
@@ -403,7 +422,8 @@
playSoundByName(customSound);
lastCustomSound = customSound;
} else if (!customSound) {
stopAlarm(); // stop any looping alarm
stopAlarm(); // stop any looping sounds
stopKlaxon();
lastCustomSound = null;
}
@@ -434,7 +454,8 @@
// Handle tap - enable sound and show reaction
document.body.addEventListener("click", () => {
stopAlarm(); // stop any looping alarm
stopAlarm(); // stop any looping sounds
stopKlaxon();
// Enable sound on first tap (browser autoplay policy)
if (!soundEnabled) {
soundEnabled = true;

View File

@@ -11,7 +11,7 @@ BASE_URL = sys.argv[1].rstrip("/") if len(sys.argv) > 1 else "http://localhost:5
SOUNDS = [
"chime", "alert", "warning", "critical", "success", "notify",
"doorbell", "knock", "ding", "blip", "siren", "tada",
"ping", "bubble", "fanfare", "alarm", "none",
"ping", "bubble", "fanfare", "alarm", "klaxon", "none",
]
FACES = [
@@ -43,12 +43,12 @@ CONTROLS = [
]
def post(path: str, data: dict | None = None) -> bool:
def post(path: str, data: dict | None = None) -> tuple[bool, str]:
try:
resp = requests.post(f"{BASE_URL}/{path}", json=data or {}, timeout=5)
return resp.ok
except Exception:
return False
return resp.ok, "" if resp.ok else f"HTTP {resp.status_code}: {resp.text[:80]}"
except Exception as e:
return False, str(e)
class KaoTUI(App):
@@ -95,37 +95,37 @@ class KaoTUI(App):
if item_id.startswith("sound-"):
name = item_id[len("sound-"):]
ok = post("notify", {"message": f"sound: {name}", "sound": name, "duration": 5})
self.notify(f"{name} {'sent' if ok else 'FAILED'}", severity="information" if ok else "error")
ok, err = post("notify", {"message": f"sound: {name}", "sound": name, "duration": 2})
self.notify(f"{name} sent" if ok else f"{name} FAILED: {err}", severity="information" if ok else "error")
elif item_id.startswith("face-"):
desc = item_id[len("face-"):]
face = next((f for f in FACES if f["desc"] == desc), None)
if face:
ok = post("notify", {
ok, err = post("notify", {
"emote": face["emote"],
"animation": face["animation"],
"color": face["color"],
"message": f"face: {face['desc']}",
"duration": 5,
"duration": 2,
})
self.notify(f"{face['emote']} {face['desc']} {'sent' if ok else 'FAILED'}", severity="information" if ok else "error")
self.notify(f"{face['emote']} {face['desc']} sent" if ok else f"{face['desc']} FAILED: {err}", severity="information" if ok else "error")
elif item_id.startswith("event-"):
suffix = item_id[len("event-"):]
if suffix == "clearall":
ok = post("clear-all")
self.notify(f"clear-all {'sent' if ok else 'FAILED'}", severity="information" if ok else "error")
ok, err = post("clear-all")
self.notify("clear-all sent" if ok else f"clear-all FAILED: {err}", severity="information" if ok else "error")
else:
ev = next((e for e in EVENTS if e["id"] == suffix), None)
if ev:
ok = post("event", {"id": ev["id"], "priority": ev["priority"], "message": ev["message"], "ttl": 10})
self.notify(f"{ev['label'].strip()} {'sent' if ok else 'FAILED'}", severity="information" if ok else "error")
ok, err = post("event", {"id": ev["id"], "priority": ev["priority"], "message": ev["message"], "ttl": 10})
self.notify(f"{ev['label'].strip()} sent" if ok else f"{ev['label'].strip()} FAILED: {err}", severity="information" if ok else "error")
elif item_id.startswith("ctrl-"):
action = item_id[len("ctrl-"):]
ok = post(action)
self.notify(f"{action} {'sent' if ok else 'FAILED'}", severity="information" if ok else "error")
ok, err = post(action)
self.notify(f"{action} sent" if ok else f"{action} FAILED: {err}", severity="information" if ok else "error")
if __name__ == "__main__":

View File

@@ -14,7 +14,7 @@ info:
## TTL/Heartbeat Pattern
Events can have a TTL (time-to-live) that auto-expires them. Detectors typically send
heartbeat events that expire if not refreshed, indicating loss of communication.
version: 1.5.0
version: 2.2.0
license:
name: MIT
@@ -323,6 +323,7 @@ components:
- bubble
- fanfare
- alarm
- klaxon
- none
example: "chime"