Add sleep mode and documentation
- Sleep/wake endpoints for Home Assistant webhooks - Sleep state: dim ( -_-)zzZ with slow breathing animation - Updated CLAUDE.md with full technical reference - Added README.md with user guide and HA integration examples Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
61
CLAUDE.md
61
CLAUDE.md
@@ -25,12 +25,9 @@ Sentry-Emote is a minimalist system status monitor designed for an old Pixel pho
|
|||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Setup
|
|
||||||
python -m venv venv
|
python -m venv venv
|
||||||
source venv/bin/activate # or .\venv\Scripts\activate on Windows
|
source venv/bin/activate # or .\venv\Scripts\activate on Windows
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
# Run everything
|
|
||||||
python sentry.py
|
python sentry.py
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -38,7 +35,7 @@ UI available at http://localhost:5000
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Edit `config.json` to configure the aggregator URL, enable/disable detectors, and set thresholds.
|
Edit `config.json` to configure detectors:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -49,11 +46,7 @@ Edit `config.json` to configure the aggregator URL, enable/disable detectors, an
|
|||||||
"name": "cpu",
|
"name": "cpu",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"script": "detectors/cpu.py",
|
"script": "detectors/cpu.py",
|
||||||
"env": {
|
"env": { "CHECK_INTERVAL": "30", "THRESHOLD_WARNING": "85", "THRESHOLD_CRITICAL": "95" }
|
||||||
"CHECK_INTERVAL": "30",
|
|
||||||
"THRESHOLD_WARNING": "85",
|
|
||||||
"THRESHOLD_CRITICAL": "95"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -73,35 +66,43 @@ All detectors support: `AGGREGATOR_URL`, `CHECK_INTERVAL`, `THRESHOLD_WARNING`,
|
|||||||
|
|
||||||
## API Endpoints
|
## API Endpoints
|
||||||
|
|
||||||
- `POST /event` — Register event: `{"id": "name", "priority": 1-4, "message": "optional", "ttl": optional_seconds}`
|
| Endpoint | Method | Description |
|
||||||
- `POST /clear` — Clear event: `{"id": "name"}`
|
|----------|--------|-------------|
|
||||||
- `GET /status` — Current state JSON
|
| `/event` | POST | Register event: `{"id": "name", "priority": 1-4, "message": "optional", "ttl": seconds}` |
|
||||||
- `GET /events` — List active events
|
| `/clear` | POST | Clear event: `{"id": "name"}` |
|
||||||
|
| `/sleep` | POST | Enter sleep mode (for Home Assistant) |
|
||||||
|
| `/wake` | POST | Exit sleep mode |
|
||||||
|
| `/status` | GET | Current state JSON |
|
||||||
|
| `/events` | GET | List active events |
|
||||||
|
|
||||||
## Priority System
|
## Priority System
|
||||||
|
|
||||||
Lower number = higher priority. Events with a `ttl` auto-expire (heartbeat pattern).
|
Lower number = higher priority. Events with a `ttl` auto-expire (heartbeat pattern).
|
||||||
|
|
||||||
| Priority | State | Emote | Color | Behavior |
|
| Priority | State | Emote | Color | Animation |
|
||||||
|----------|----------|----------|--------|----------|
|
|----------|-------|-------|-------|-----------|
|
||||||
| 1 | Critical | `( x_x)` | Red | Shaking animation |
|
| 1 | Critical | `( x_x)` | Red | shaking |
|
||||||
| 2 | Warning | `( o_o)` | Yellow | Breathing animation |
|
| 2 | Warning | `( o_o)` | Yellow | breathing |
|
||||||
| 3 | Notify | `( 'o')` | Blue | Popping animation, 10s default TTL |
|
| 3 | Notify | `( 'o')` | Blue | popping |
|
||||||
| 4 | Optimal | `( ^_^)` | Green | Default when no events |
|
| 4 | Optimal | varies | Green | varies |
|
||||||
|
|
||||||
## Testing Events
|
## Personality System
|
||||||
|
|
||||||
```bash
|
The optimal state cycles through emotes with paired animations every 5 minutes:
|
||||||
# Warning with 30s TTL
|
|
||||||
curl -X POST -H "Content-Type: application/json" \
|
|
||||||
-d '{"id":"test","priority":2,"message":"Test warning","ttl":30}' \
|
|
||||||
http://localhost:5000/event
|
|
||||||
|
|
||||||
# Clear manually
|
| Emote | Animation | Vibe |
|
||||||
curl -X POST -H "Content-Type: application/json" \
|
|-------|-----------|------|
|
||||||
-d '{"id":"test"}' \
|
| `( ^_^)` | breathing | calm |
|
||||||
http://localhost:5000/clear
|
| `( ᵔᴥᵔ)` | floating | dreamy |
|
||||||
```
|
| `(◕‿◕)` | bouncing | cheerful |
|
||||||
|
| `( ・ω・)` | swaying | curious |
|
||||||
|
| `( ˘▽˘)` | breathing | cozy |
|
||||||
|
|
||||||
|
Additional states:
|
||||||
|
- **Idle expressions** (15% chance): `( -_^)`, `( ^_~)`, `( ᵕ.ᵕ)` with blink animation
|
||||||
|
- **Recovery celebration**: `\(^o^)/` with bounce for 5 seconds after issues resolve
|
||||||
|
- **Connection lost**: `( ?.?)` gray, searching animation
|
||||||
|
- **Sleep mode**: `( -_-)zzZ` dim, very slow breathing
|
||||||
|
|
||||||
## File Structure
|
## File Structure
|
||||||
|
|
||||||
|
|||||||
155
README.md
Normal file
155
README.md
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# Sentry-Emote
|
||||||
|
|
||||||
|
A minimalist system status monitor that uses ASCII emotes to display server health on an old phone.
|
||||||
|
|
||||||
|
-brightgreen)
|
||||||
|
|
||||||
|
## 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
|
||||||
|
- **Home Assistant ready** — Webhook endpoints for automation
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone and setup
|
||||||
|
git clone https://github.com/yourusername/sentry-emote.git
|
||||||
|
cd sentry-emote
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate # Windows: .\venv\Scripts\activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Run everything
|
||||||
|
python sentry.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Open http://localhost:5000 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) |
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Edit `config.json` to enable/disable detectors and set thresholds:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"aggregator_url": "http://localhost:5000",
|
||||||
|
"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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:5000/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:
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:5000/clear \
|
||||||
|
-d '{"id": "my_check"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Home Assistant Integration
|
||||||
|
|
||||||
|
Add webhook commands to your Home Assistant config:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
rest_command:
|
||||||
|
sentry_sleep:
|
||||||
|
url: "http://YOUR_SERVER:5000/sleep"
|
||||||
|
method: POST
|
||||||
|
sentry_wake:
|
||||||
|
url: "http://YOUR_SERVER:5000/wake"
|
||||||
|
method: POST
|
||||||
|
```
|
||||||
|
|
||||||
|
Trigger from automations:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
automation:
|
||||||
|
- alias: "Sentry Sleep at Bedtime"
|
||||||
|
trigger:
|
||||||
|
platform: time
|
||||||
|
at: "23:00:00"
|
||||||
|
action:
|
||||||
|
service: rest_command.sentry_sleep
|
||||||
|
|
||||||
|
- alias: "Sentry Wake in Morning"
|
||||||
|
trigger:
|
||||||
|
platform: time
|
||||||
|
at: "07:00:00"
|
||||||
|
action:
|
||||||
|
service: rest_command.sentry_wake
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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 |
|
||||||
|
| `/sleep` | POST | Enter sleep mode |
|
||||||
|
| `/wake` | POST | Exit sleep mode |
|
||||||
|
|
||||||
|
## Personality
|
||||||
|
|
||||||
|
The emote has personality! In optimal state it:
|
||||||
|
|
||||||
|
- Rotates through happy faces every 5 minutes
|
||||||
|
- Occasionally winks `( -_^)` or blinks `( ᵕ.ᵕ)`
|
||||||
|
- Celebrates `\(^o^)/` when recovering from warnings
|
||||||
|
- Each face has its own animation (floating, bouncing, swaying)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
@@ -53,11 +53,29 @@ last_emote_change = 0
|
|||||||
current_optimal_emote = OPTIMAL_EMOTES[0][0]
|
current_optimal_emote = OPTIMAL_EMOTES[0][0]
|
||||||
current_optimal_animation = OPTIMAL_EMOTES[0][1]
|
current_optimal_animation = OPTIMAL_EMOTES[0][1]
|
||||||
|
|
||||||
|
# Sleep mode
|
||||||
|
is_sleeping = False
|
||||||
|
SLEEP_EMOTE = "( -_-)zzZ"
|
||||||
|
SLEEP_COLOR = "#333333"
|
||||||
|
SLEEP_ANIMATION = "sleeping"
|
||||||
|
|
||||||
|
|
||||||
def get_current_state():
|
def get_current_state():
|
||||||
"""Determine current state based on active events."""
|
"""Determine current state based on active events."""
|
||||||
global previous_priority, celebrating_until, last_emote_change, current_optimal_emote, current_optimal_animation
|
global previous_priority, celebrating_until, last_emote_change, current_optimal_emote, current_optimal_animation
|
||||||
|
|
||||||
|
# Sleep mode overrides everything
|
||||||
|
if is_sleeping:
|
||||||
|
return {
|
||||||
|
"current_state": "sleeping",
|
||||||
|
"active_emote": SLEEP_EMOTE,
|
||||||
|
"color": SLEEP_COLOR,
|
||||||
|
"animation": SLEEP_ANIMATION,
|
||||||
|
"message": "",
|
||||||
|
"active_events": [],
|
||||||
|
"last_updated": datetime.now().isoformat(timespec="seconds"),
|
||||||
|
}
|
||||||
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
|
||||||
with events_lock:
|
with events_lock:
|
||||||
@@ -196,6 +214,24 @@ def clear_event():
|
|||||||
return jsonify({"error": "Event not found"}), 404
|
return jsonify({"error": "Event not found"}), 404
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/sleep", methods=["POST"])
|
||||||
|
def sleep_mode():
|
||||||
|
"""Enter sleep mode. For Home Assistant webhook."""
|
||||||
|
global is_sleeping
|
||||||
|
is_sleeping = True
|
||||||
|
state = write_status()
|
||||||
|
return jsonify({"status": "sleeping", "current_state": state}), 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/wake", methods=["POST"])
|
||||||
|
def wake_mode():
|
||||||
|
"""Exit sleep mode. For Home Assistant webhook."""
|
||||||
|
global is_sleeping
|
||||||
|
is_sleeping = False
|
||||||
|
state = write_status()
|
||||||
|
return jsonify({"status": "awake", "current_state": state}), 200
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
"""Serve the frontend."""
|
"""Serve the frontend."""
|
||||||
|
|||||||
16
index.html
16
index.html
@@ -172,6 +172,22 @@
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Sleeping animation - very slow, subtle breathing */
|
||||||
|
.sleeping {
|
||||||
|
animation: sleep 6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sleep {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 0.4;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.2;
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Reference in New Issue
Block a user