Spencer aaae20281d Bump to v2.3.0: replace polling with SSE stream, fix detector imports
- Add GET /stream SSE endpoint to aggregator.py; state is pushed
  instantly on every change instead of fetched every 2s
- Replace setInterval polling in index.html with EventSource;
  onerror shows the ( ?.?) face and auto-reconnect is handled by
  the browser natively
- Fix ModuleNotFoundError in detectors: inject project root into
  PYTHONPATH when launching subprocesses from kao.py
- Update openapi.yaml, CLAUDE.md, README.md with /stream endpoint
- Remove completed SSE item from TODO.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 17:25:15 -06:00
2026-02-20 17:12:39 -06:00

Kao

A minimalist system status monitor that uses ASCII emotes to display server health on an old phone.

Status: Optimal

Why?

Turn an old phone (with its OLED screen) into a glanceable ambient display for your home server. Instead of graphs and numbers, see a happy face ( ^_^) when things are good, and a worried face ( o_o) when they're not.

Features

  • OLED-optimized — Pure black background, saves battery
  • Glanceable — Know your server's status from across the room
  • Instant updates — SSE stream pushes state changes the moment they happen
  • Extensible — Add custom detectors for any metric
  • Personality — Rotating expressions, celebration animations, sleep mode
  • Sound effects — Optional audio cues for state changes (tap to enable)
  • Home Assistant ready — Webhook endpoints for notifications and automation

Quick Start

# Clone and setup
git clone https://github.com/yourusername/kao.git
cd kao
python -m venv venv
source venv/bin/activate  # Windows: .\venv\Scripts\activate
pip install -r requirements.txt

# Run everything
python kao.py

Open http://localhost:5100 on your phone (use Fully Kiosk Browser for best results).

Activating the venv

Two venvs exist — one for Windows, one for WSL:

# WSL / Linux
source venv-wsl/bin/activate

# Windows (PowerShell)
.\venv\Scripts\Activate.ps1

# Windows (cmd)
.\venv\Scripts\activate.bat

Deactivate either with deactivate.

Status Faces

State Emote Meaning
Optimal ( ^_^) All systems healthy
Warning ( o_o) Something needs attention
Critical ( x_x) Immediate action required
Notify ( 'o') Transient notification
Sleeping ( -_-)zzZ Sleep mode active
Disconnected ( ?.?) Can't reach server

Built-in Detectors

Detector Monitors
disk_space Disk usage on all drives
cpu CPU utilization
memory RAM usage
service Whether processes are running
network Host reachability (ping)
docker Container health and restart loops

Configuration

Edit config.json to enable/disable detectors and set thresholds:

{
	"aggregator_url": "http://localhost:5100",
	"detectors": [
		{
			"name": "disk_space",
			"enabled": true,
			"script": "detectors/disk_space.py",
			"env": {
				"CHECK_INTERVAL": "300",
				"THRESHOLD_WARNING": "85",
				"THRESHOLD_CRITICAL": "95"
			}
		}
	]
}

Custom Detectors

Create your own detector by POSTing events to the aggregator:

curl -X POST http://localhost:5100/event \
  -H "Content-Type: application/json" \
  -d '{"id": "my_check", "priority": 2, "message": "Something is wrong", "ttl": 120}'
  • id — Unique identifier for this event
  • priority — 1 (critical), 2 (warning), 3 (notify), 4 (optimal)
  • message — What to display
  • ttl — Auto-expire after N seconds (for heartbeat pattern)

Clear an event:

curl -X POST http://localhost:5100/clear \
  -d '{"id": "my_check"}'

Home Assistant Integration

Add REST commands to your configuration.yaml:

rest_command:
  kao_notify:
    url: "http://YOUR_SERVER:5100/notify"
    method: POST
    content_type: "application/json"
    payload: >
      {
        "message": "{{ message | default('') }}",
        "duration": {{ duration | default(5) }},
        "emote": "{{ emote | default('') }}",
        "color": "{{ color | default('') }}",
        "animation": "{{ animation | default('') }}",
        "sound": "{{ sound | default('') }}"
      }

  kao_sleep:
    url: "http://YOUR_SERVER:5100/sleep"
    method: POST

  kao_wake:
    url: "http://YOUR_SERVER:5100/wake"
    method: POST

Use in automations:

automation:
  - alias: "Doorbell Notification"
    trigger:
      platform: state
      entity_id: binary_sensor.doorbell
      to: "on"
    action:
      service: rest_command.kao_notify
      data:
        message: "Someone at the door"
        duration: 10
        emote: "( °o°)"
        color: "#FF9900"
        sound: "chime"

  - alias: "Timer Complete"
    trigger:
      platform: event
      event_type: timer.finished
    action:
      service: rest_command.kao_notify
      data:
        message: "Timer done!"
        emote: "\\(^o^)/"
        animation: "celebrating"
        sound: "success"

  - alias: "Kao Sleep at Bedtime"
    trigger:
      platform: time
      at: "23:00:00"
    action:
      service: rest_command.kao_sleep

  - alias: "Kao Wake in Morning"
    trigger:
      platform: time
      at: "07:00:00"
    action:
      service: rest_command.kao_wake

Notify options:

  • 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, klaxon, none

Developer TUI

kao_tui.py is a terminal UI for firing test events, faces, and sounds at a running Kao instance — no curl needed.

python kao_tui.py                        # connect to http://localhost:5100
python kao_tui.py http://192.168.1.x:5100  # custom URL

Four tabs:

  • 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

Navigate with ↑↓ or Tab, press Enter to fire, Q to quit. A toast confirms each action.

API Reference

Endpoint Method Description
/ GET Web UI
/stream GET SSE stream — pushes state JSON on every change
/status GET Current state as JSON (one-shot query)
/events GET List all active events
/event POST Register an event
/clear POST Clear an event by ID
/clear-all POST Clear all active events
/notify POST Simple notification {"message": "", "duration": 5}
/sleep POST Enter sleep mode
/wake POST Exit sleep mode
/docs GET Interactive API documentation (Swagger UI)

Full API documentation available at /docs or in openapi.yaml.

Personality

The emote has personality! In optimal state it:

  • Shows a stable face all day — set fresh each morning when /wake is called
  • Occasionally winks ( -_^) or blinks ( ᵕ.ᵕ) for a second or two on wake
  • Celebrates \(^o^)/ when recovering from warnings
  • Each face has its own animation (floating, bouncing, swaying)
  • Reacts when tapped ( °o°), shows version info, and dismisses active alerts

Sound effects (tap screen to enable, or use ?sound=on):

  • Warning: soft double-beep
  • Critical: urgent descending tone
  • Notify: gentle ping
  • Recovery: happy ascending chirp
  • 11 additional synthesized sounds for /notify: doorbell, knock, ding, blip, siren, tada, ping, bubble, fanfare, alarm, klaxon (alarm and klaxon loop until tapped)

License

MIT

Description
No description provided
Readme 333 KiB
Languages
Python 73.6%
HTML 19.4%
Shell 7%