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>
This commit is contained in:
2026-02-06 12:17:17 -06:00
parent c3ceb74ce8
commit dd8bf6005b
12 changed files with 126 additions and 236 deletions

View File

@@ -19,6 +19,9 @@ ROOT_DIR = Path(__file__).parent
STATUS_FILE = Path(__file__).parent / "status.json"
DEFAULT_NOTIFY_TTL = 10 # Default TTL for Priority 3 (Notify) events
CELEBRATION_DURATION = 5 # Seconds to show celebration after recovery
EMOTE_ROTATION_INTERVAL = 300 # Seconds between emote rotations
IDLE_EXPRESSION_CHANCE = 0.15 # Chance of a brief blink/wink on rotation
DEFAULT_NOTIFY_DURATION = 5 # Default duration for /notify events
# Emote variations with paired animations
OPTIMAL_EMOTES = [
@@ -53,10 +56,13 @@ celebrating_until = 0
blinking_until = 0
blink_emote = None
blink_animation = None
last_emote_change = 0
last_emote_change = time.time()
current_optimal_emote = OPTIMAL_EMOTES[0][0]
current_optimal_animation = OPTIMAL_EMOTES[0][1]
# Notify counter for unique IDs
_notify_counter = 0
# Sleep mode
is_sleeping = False
SLEEP_EMOTE = "( -_-)zzZ"
@@ -134,11 +140,11 @@ def get_current_state():
animation = blink_animation
else:
# Rotate optimal emotes every 5 minutes
if now - last_emote_change > 300:
if now - last_emote_change > EMOTE_ROTATION_INTERVAL:
last_emote_change = now
current_optimal_emote, current_optimal_animation = random.choice(OPTIMAL_EMOTES)
# 15% chance of a brief blink/wink
if random.random() < 0.15:
# Brief blink/wink chance on rotation
if random.random() < IDLE_EXPRESSION_CHANCE:
blink_emote, blink_animation = random.choice(IDLE_EMOTES)
blinking_until = now + random.uniform(1, 2)
emote = current_optimal_emote
@@ -163,8 +169,11 @@ def get_current_state():
def write_status():
"""Write current state to status.json."""
state = get_current_state()
with open(STATUS_FILE, "w") as f:
json.dump(state, f, indent="\t")
try:
with open(STATUS_FILE, "w") as f:
json.dump(state, f, indent="\t")
except OSError as e:
print(f"[ERROR] Failed to write status file: {e}")
return state
@@ -272,12 +281,14 @@ def notify():
"sound": "chime" # optional: chime, alert, warning, critical, success, none
}
"""
global _notify_counter
data = request.get_json(force=True) if request.data else {}
message = data.get("message", "")
duration = int(data.get("duration", 5))
duration = int(data.get("duration", DEFAULT_NOTIFY_DURATION))
# Generate unique ID to avoid conflicts
event_id = f"ha_notify_{int(time.time() * 1000)}"
_notify_counter += 1
event_id = f"notify_{int(time.time())}_{_notify_counter}"
event = {
"priority": 3, # Notify priority