Files
snek/dashboard/sensit_decoder.py
T
tarcourt a77776d6d8 feat: web dashboard for live device data
Small companion service that receives SNEK callbacks, decodes Sens'it
Discovery payloads for all 6 modes (Standby, Temperature, Light, Door,
Vibration, Magnet), stores in SQLite, and serves a responsive dashboard.

Backend (Python Flask):
- POST /webhook: receive SNEK callback
- GET /api/devices, /api/messages, /api/decode
- GET /events: Server-Sent Events for live push
- SQLite persistence at /data/messages.db

Frontend:
- Tailwind CSS via CDN, Chart.js for temp/humidity graph
- Live updates via SSE
- Device cards with icons per mode
- Message log with human-readable summaries

Deploy:
- Separate deployment (snek-dashboard) with own PVC
- LoadBalancer on 192.168.1.213:80
- SNEK callback URL: http://snek-dashboard.snek.svc.cluster.local/webhook

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-03 17:43:11 +02:00

87 lines
2.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Sens'it Discovery payload decoder — all 6 modes."""
MODES = {
0: "Standby / Button",
1: "Temperature",
2: "Light",
3: "Door",
4: "Vibration",
5: "Magnet",
}
def decode(payload_hex: str) -> dict:
"""
Decode a 4-byte Sens'it Discovery payload.
Format (per Sens'it Discovery v3.1.0+ spec):
Byte 0: bits 7-3 = battery level, bits 2-0 = reserved (0b110)
Byte 1: bits 7-3 = mode, bits 2-1 = mode-specific / spare, bit 0 = button flag
Byte 2-3: mode-specific data
Battery formula: V = (raw × 0.05) + 2.7
"""
payload_hex = payload_hex.strip().lower().replace("0x", "").replace(" ", "")
if len(payload_hex) != 8:
return {"error": f"payload must be 4 bytes (8 hex chars), got '{payload_hex}'"}
try:
b0, b1, b2, b3 = (int(payload_hex[i : i + 2], 16) for i in range(0, 8, 2))
except ValueError as e:
return {"error": str(e)}
# Common fields
battery_raw = (b0 >> 3) & 0x1F
battery_volts = round(battery_raw * 0.05 + 2.7, 2)
reserved = b0 & 0x07
mode = (b1 >> 3) & 0x1F
button_alert = bool(b1 & 0x01)
result = {
"raw": payload_hex,
"battery": battery_volts,
"battery_raw": battery_raw,
"reserved_ok": reserved == 0b110,
"mode": MODES.get(mode, f"Unknown ({mode})"),
"mode_id": mode,
"button_alert": button_alert,
}
# Mode-specific decoding
if mode == 1: # Temperature
temp_msb = (b1 >> 2) & 0x01
temp_raw = (temp_msb << 8) | b2
result["temperature_c"] = round((temp_raw - 200) / 8, 2)
result["humidity_pct"] = round(b3 / 2, 1)
elif mode == 2: # Light
brightness_msb = (b1 >> 2) & 0x01
brightness_raw = (brightness_msb << 8) | b2
result["brightness_lux"] = round(brightness_raw / 96, 2)
elif mode == 3: # Door
door_status_bits = (b1 >> 1) & 0x03
door_map = {0: "closed", 1: "opened", 2: "opening_alert", 3: "closing_alert"}
result["door_status"] = door_map.get(door_status_bits, "unknown")
result["event_count"] = (b2 << 8) | b3
elif mode == 4: # Vibration
vib_status_bits = (b1 >> 1) & 0x03
vib_map = {0: "no_vibration", 1: "vibration_detected"}
result["vibration_status"] = vib_map.get(vib_status_bits, "unknown")
result["event_count"] = (b2 << 8) | b3
elif mode == 5: # Magnet
mag_status_bits = (b1 >> 1) & 0x03
mag_map = {0: "no_magnet", 1: "magnet_detected"}
result["magnet_status"] = mag_map.get(mag_status_bits, "unknown")
result["event_count"] = (b2 << 8) | b3
return result
if __name__ == "__main__":
# Sanity checks with the payloads we captured
for h in ("b60dc86e", "a60dc271", "b6043081"):
print(h, "", decode(h))