Spencer Grimes dd8bf6005b Bump to v1.5.0: deduplicate detectors, fix aggregator bugs, fix blocking I/O
- Extract shared send_event/clear_event into detectors/base.py, removing
  ~150 lines of duplication across all 6 detectors
- Fix default aggregator URL from port 5000 to 5100 in all detectors
- Standardize cpu.py and memory.py to use active_alerts set pattern
- Fix immediate emote rotation on startup (last_emote_change = time.time())
- Extract magic numbers to named constants in aggregator
- Protect write_status() with try/except OSError
- Fix notify event ID collision with monotonic counter
- Replace blocking stream_output() with background daemon threads in kao.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 12:17:17 -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
  • 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).

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

API Reference

Endpoint Method Description
/ GET Web UI
/status GET Current state as JSON
/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:

  • Rotates through happy faces every 5 minutes
  • Occasionally winks ( -_^) or blinks ( ᵕ.ᵕ) for a second or two
  • 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

License

MIT

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