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:
@@ -103,7 +103,7 @@ All detectors support: `AGGREGATOR_URL`, `CHECK_INTERVAL`, `THRESHOLD_WARNING`,
|
|||||||
| `emote` | No | Custom emote to display |
|
| `emote` | No | Custom emote to display |
|
||||||
| `color` | No | Custom color (hex, e.g., `#FF9900`) |
|
| `color` | No | Custom color (hex, e.g., `#FF9900`) |
|
||||||
| `animation` | No | One of: `breathing`, `shaking`, `popping`, `celebrating`, `floating`, `bouncing`, `swaying` |
|
| `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
|
## Priority System
|
||||||
|
|
||||||
@@ -123,7 +123,6 @@ The optimal state face is set once per day on `/wake` (random pick). Each mornin
|
|||||||
| Emote | Animation | Vibe |
|
| Emote | Animation | Vibe |
|
||||||
|-------|-----------|------|
|
|-------|-----------|------|
|
||||||
| `( ^_^)` | breathing | calm |
|
| `( ^_^)` | breathing | calm |
|
||||||
| `( ˙▿˙)` | floating | content |
|
|
||||||
| `(◕‿◕)` | bouncing | cheerful |
|
| `(◕‿◕)` | bouncing | cheerful |
|
||||||
| `( ・ω・)` | swaying | curious |
|
| `( ・ω・)` | swaying | curious |
|
||||||
| `( ˘▽˘)` | breathing | cozy |
|
| `( ˘▽˘)` | breathing | cozy |
|
||||||
@@ -177,6 +176,7 @@ Use in automations:
|
|||||||
├── kao.py # Unified entry point
|
├── kao.py # Unified entry point
|
||||||
├── aggregator.py # Event broker/API server
|
├── aggregator.py # Event broker/API server
|
||||||
├── index.html # OLED-optimized frontend
|
├── index.html # OLED-optimized frontend
|
||||||
|
├── kao_tui.py # Developer TUI for testing sounds/events
|
||||||
├── config.json # Runtime configuration
|
├── config.json # Runtime configuration
|
||||||
├── openapi.yaml # API documentation (OpenAPI 3.0)
|
├── openapi.yaml # API documentation (OpenAPI 3.0)
|
||||||
├── detectors/
|
├── detectors/
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ automation:
|
|||||||
- `emote`: Any ASCII emote (e.g., `( °o°)`, `\\(^o^)/`)
|
- `emote`: Any ASCII emote (e.g., `( °o°)`, `\\(^o^)/`)
|
||||||
- `color`: Hex color (e.g., `#FF9900`)
|
- `color`: Hex color (e.g., `#FF9900`)
|
||||||
- `animation`: `breathing`, `shaking`, `popping`, `celebrating`, `floating`, `bouncing`, `swaying`
|
- `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
|
## Developer TUI
|
||||||
|
|
||||||
@@ -206,7 +206,7 @@ python kao_tui.py http://192.168.1.x:5100 # custom URL
|
|||||||
```
|
```
|
||||||
|
|
||||||
Four tabs:
|
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`
|
- **Faces** — send any preset emote/animation combo via `/notify`
|
||||||
- **Events** — post Critical/Warning/Notify events (10s TTL) or clear all
|
- **Events** — post Critical/Warning/Notify events (10s TTL) or clear all
|
||||||
- **Controls** — Sleep, Wake, Clear all
|
- **Controls** — Sleep, Wake, Clear all
|
||||||
@@ -245,7 +245,7 @@ The emote has personality! In optimal state it:
|
|||||||
- Critical: urgent descending tone
|
- Critical: urgent descending tone
|
||||||
- Notify: gentle ping
|
- Notify: gentle ping
|
||||||
- Recovery: happy ascending chirp
|
- 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
|
## License
|
||||||
|
|
||||||
|
|||||||
39
index.html
39
index.html
@@ -217,7 +217,7 @@
|
|||||||
const emoteEl = document.getElementById("emote");
|
const emoteEl = document.getElementById("emote");
|
||||||
const messageEl = document.getElementById("message");
|
const messageEl = document.getElementById("message");
|
||||||
const POLL_INTERVAL = 2000;
|
const POLL_INTERVAL = 2000;
|
||||||
const VERSION = "v2.1.0";
|
const VERSION = "v2.2.0";
|
||||||
|
|
||||||
// Sound system
|
// Sound system
|
||||||
let audioCtx = null;
|
let audioCtx = null;
|
||||||
@@ -254,9 +254,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function playWarningSound() {
|
function playWarningSound() {
|
||||||
// Soft double-beep
|
// Two-tone warning beep - differentiated pitches
|
||||||
playTone(440, 0.15);
|
playTone(440, 0.2, "sine", 0.18);
|
||||||
setTimeout(() => playTone(440, 0.15), 180);
|
setTimeout(() => playTone(550, 0.2, "sine", 0.18), 220);
|
||||||
}
|
}
|
||||||
|
|
||||||
function playCriticalSound() {
|
function playCriticalSound() {
|
||||||
@@ -334,8 +334,8 @@
|
|||||||
playTone(1047, 0.15, "sine", 0.1);
|
playTone(1047, 0.15, "sine", 0.1);
|
||||||
}
|
}
|
||||||
function playBubbleSound() {
|
function playBubbleSound() {
|
||||||
playTone(400, 0.06, "sine", 0.06);
|
playTone(400, 0.15, "sine", 0.10);
|
||||||
setTimeout(() => playTone(600, 0.04, "sine", 0.04), 40);
|
setTimeout(() => playTone(650, 0.12, "sine", 0.08), 100);
|
||||||
}
|
}
|
||||||
function playFanfareSound() {
|
function playFanfareSound() {
|
||||||
playTone(523, 0.1, "sine", 0.1);
|
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
|
// Play sound by name
|
||||||
function playSoundByName(name) {
|
function playSoundByName(name) {
|
||||||
if (name === "alarm") { startAlarm(); return; }
|
if (name === "alarm") { startAlarm(); return; }
|
||||||
|
if (name === "klaxon") { startKlaxon(); return; }
|
||||||
const sounds = {
|
const sounds = {
|
||||||
chime: playChimeSound,
|
chime: playChimeSound,
|
||||||
alert: playAlertSound,
|
alert: playAlertSound,
|
||||||
@@ -403,7 +422,8 @@
|
|||||||
playSoundByName(customSound);
|
playSoundByName(customSound);
|
||||||
lastCustomSound = customSound;
|
lastCustomSound = customSound;
|
||||||
} else if (!customSound) {
|
} else if (!customSound) {
|
||||||
stopAlarm(); // stop any looping alarm
|
stopAlarm(); // stop any looping sounds
|
||||||
|
stopKlaxon();
|
||||||
lastCustomSound = null;
|
lastCustomSound = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,7 +454,8 @@
|
|||||||
|
|
||||||
// Handle tap - enable sound and show reaction
|
// Handle tap - enable sound and show reaction
|
||||||
document.body.addEventListener("click", () => {
|
document.body.addEventListener("click", () => {
|
||||||
stopAlarm(); // stop any looping alarm
|
stopAlarm(); // stop any looping sounds
|
||||||
|
stopKlaxon();
|
||||||
// Enable sound on first tap (browser autoplay policy)
|
// Enable sound on first tap (browser autoplay policy)
|
||||||
if (!soundEnabled) {
|
if (!soundEnabled) {
|
||||||
soundEnabled = true;
|
soundEnabled = true;
|
||||||
|
|||||||
32
kao_tui.py
32
kao_tui.py
@@ -11,7 +11,7 @@ BASE_URL = sys.argv[1].rstrip("/") if len(sys.argv) > 1 else "http://localhost:5
|
|||||||
SOUNDS = [
|
SOUNDS = [
|
||||||
"chime", "alert", "warning", "critical", "success", "notify",
|
"chime", "alert", "warning", "critical", "success", "notify",
|
||||||
"doorbell", "knock", "ding", "blip", "siren", "tada",
|
"doorbell", "knock", "ding", "blip", "siren", "tada",
|
||||||
"ping", "bubble", "fanfare", "alarm", "none",
|
"ping", "bubble", "fanfare", "alarm", "klaxon", "none",
|
||||||
]
|
]
|
||||||
|
|
||||||
FACES = [
|
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:
|
try:
|
||||||
resp = requests.post(f"{BASE_URL}/{path}", json=data or {}, timeout=5)
|
resp = requests.post(f"{BASE_URL}/{path}", json=data or {}, timeout=5)
|
||||||
return resp.ok
|
return resp.ok, "" if resp.ok else f"HTTP {resp.status_code}: {resp.text[:80]}"
|
||||||
except Exception:
|
except Exception as e:
|
||||||
return False
|
return False, str(e)
|
||||||
|
|
||||||
|
|
||||||
class KaoTUI(App):
|
class KaoTUI(App):
|
||||||
@@ -95,37 +95,37 @@ class KaoTUI(App):
|
|||||||
|
|
||||||
if item_id.startswith("sound-"):
|
if item_id.startswith("sound-"):
|
||||||
name = item_id[len("sound-"):]
|
name = item_id[len("sound-"):]
|
||||||
ok = post("notify", {"message": f"sound: {name}", "sound": name, "duration": 5})
|
ok, err = post("notify", {"message": f"sound: {name}", "sound": name, "duration": 2})
|
||||||
self.notify(f"♪ {name} {'sent' if ok else 'FAILED'}", severity="information" if ok else "error")
|
self.notify(f"♪ {name} sent" if ok else f"♪ {name} FAILED: {err}", severity="information" if ok else "error")
|
||||||
|
|
||||||
elif item_id.startswith("face-"):
|
elif item_id.startswith("face-"):
|
||||||
desc = item_id[len("face-"):]
|
desc = item_id[len("face-"):]
|
||||||
face = next((f for f in FACES if f["desc"] == desc), None)
|
face = next((f for f in FACES if f["desc"] == desc), None)
|
||||||
if face:
|
if face:
|
||||||
ok = post("notify", {
|
ok, err = post("notify", {
|
||||||
"emote": face["emote"],
|
"emote": face["emote"],
|
||||||
"animation": face["animation"],
|
"animation": face["animation"],
|
||||||
"color": face["color"],
|
"color": face["color"],
|
||||||
"message": f"face: {face['desc']}",
|
"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-"):
|
elif item_id.startswith("event-"):
|
||||||
suffix = item_id[len("event-"):]
|
suffix = item_id[len("event-"):]
|
||||||
if suffix == "clearall":
|
if suffix == "clearall":
|
||||||
ok = post("clear-all")
|
ok, err = post("clear-all")
|
||||||
self.notify(f"clear-all {'sent' if ok else 'FAILED'}", severity="information" if ok else "error")
|
self.notify("clear-all sent" if ok else f"clear-all FAILED: {err}", severity="information" if ok else "error")
|
||||||
else:
|
else:
|
||||||
ev = next((e for e in EVENTS if e["id"] == suffix), None)
|
ev = next((e for e in EVENTS if e["id"] == suffix), None)
|
||||||
if ev:
|
if ev:
|
||||||
ok = post("event", {"id": ev["id"], "priority": ev["priority"], "message": ev["message"], "ttl": 10})
|
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 'FAILED'}", severity="information" if ok else "error")
|
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-"):
|
elif item_id.startswith("ctrl-"):
|
||||||
action = item_id[len("ctrl-"):]
|
action = item_id[len("ctrl-"):]
|
||||||
ok = post(action)
|
ok, err = post(action)
|
||||||
self.notify(f"{action} {'sent' if ok else 'FAILED'}", severity="information" if ok else "error")
|
self.notify(f"{action} sent" if ok else f"{action} FAILED: {err}", severity="information" if ok else "error")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ info:
|
|||||||
## TTL/Heartbeat Pattern
|
## TTL/Heartbeat Pattern
|
||||||
Events can have a TTL (time-to-live) that auto-expires them. Detectors typically send
|
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.
|
heartbeat events that expire if not refreshed, indicating loss of communication.
|
||||||
version: 1.5.0
|
version: 2.2.0
|
||||||
license:
|
license:
|
||||||
name: MIT
|
name: MIT
|
||||||
|
|
||||||
@@ -323,6 +323,7 @@ components:
|
|||||||
- bubble
|
- bubble
|
||||||
- fanfare
|
- fanfare
|
||||||
- alarm
|
- alarm
|
||||||
|
- klaxon
|
||||||
- none
|
- none
|
||||||
example: "chime"
|
example: "chime"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user