A huge thank-you to @theosnel — your python-xsense turned a "nice idea" into a working control-room dashboard
We want to share what became possible thanks to the reverse-engineering work in this thread, and say a proper thank-you.
Using python-xsense we built a company-internal, standalone, self-hosted control-room ("Leitwarte") dashboard for the X-Sense security line (door/motion/smoke/water/keypad on an SBS50 base). All 21 sensors show live, and there's a floor-plan map where each sensor sits exactly where it physically hangs, colored by live status. It runs in Docker on our Synology NAS. The whole thing was built in an afternoon together with an AI coding assistant — and the single reason it went that fast is that Theo had already decoded the protocol. That groundwork was a massive relief for us. 🙏
The control-room design (the part we're most happy with)
The goal was a single glance = full situational awareness, like a real security desk.
- Floor plan as the hero element. We rendered our building's architectural PDF to an image (via poppler) and use it as the map background. Every sensor is a pin placed at its real geographic position.
- Drag-and-drop placement editor. Hit "Edit", and unplaced sensors sit in a tray; tap one, click its spot on the plan, done. Pins can be dragged to reposition, removed, and the map zooms for precise placement. Positions are stored as fractional x/y (0–1), so they're resolution-independent, and persisted server-side — they survive restarts and container rebuilds. (We deliberately don't auto-place by name — the sensor names weren't reliable enough for that.)
- Live status pins. Each pin is colored by current state and updates in real time; alarm pins pulse. Click a pin → a detail popover (name, base station, status, battery).
- Compact sensor list (left). All sensors as small rows, auto-sorted by severity so problems bubble to the top. Click a row → the map scrolls to that pin and highlights it — very handy when something trips.
- Status bar + event feed. A header shows a global status light plus counts (alarm / warning / offline) and a live-connection indicator; an event feed logs every state change (e.g. "Door 4: closed → open").
- One WebSocket, everything in sync. The backend polls every 30 s, diffs against the previous snapshot, and pushes one message; the map, the list and the event feed all update from it together.
- Rename-safe. Devices are keyed by serial number, not name, so renaming a sensor in the X-Sense app never moves its pin on the map.
Architecture
X-Sense Cloud ──(python-xsense: Cognito SRP + REST)──► Python service (FastAPI/uvicorn)
│ poll every 30s, diff changes
├─ /api/state (JSON snapshot)
├─ /ws (live push)
├─ /api/layout (sensor positions)
└─ / (dashboard)
▼
Browser dashboard (vanilla JS + WebSocket)
How python-xsense is used (for anyone starting out)
AsyncXSense() → init() → login(user, pass) (AWS Cognito user pool, SRP) → load_all().
- Per cycle:
get_house_state(), get_station_state(), get_state(), get_alarm_state().
- We do REST polling every 30 s, not MQTT. (The library also exposes the AWS creds for MQTT-over-WebSocket real-time shadows via bizCode 101003 — we simply didn't need it for our refresh rate.)
Device types & fields we observed (security line)
Might save the next person some digging:
| Type code |
Device |
Key status field(s) |
SDS0A |
Door/window contact |
isOpen ("1" = open), openRemind |
SMS0A |
PIR motion |
isMoved ("1" = motion), isArmed |
XS0B-MR |
Smoke detector |
alarmStatus, isLifeEnd |
SWS51 |
Water leak |
alarmStatus |
SKP0A |
Keypad |
alarmStatus |
SBS50 |
Base station |
arm mode via _alarm_data.mode (Disarmed/Armed) |
Common fields on every device: online ("1"), batInfo (0–3 battery level), rfLevel (1–3 signal), alarmStatus (bool).
Gotchas we hit (hopefully useful)
- Python ≥ 3.10 required — the library uses PEP 604
str | None syntax; it won't import on 3.9.
aiohttp is an extra dependency for the async client — install it explicitly.
- The installed version of
AsyncXSense has no close() method — guard your shutdown.
- One active session per account. The service login will kick your phone app off (and vice-versa) with
SessionExpired: another device is logged in!. Fix: create a dedicated X-Sense account, share the home to it, and use that only for the integration — never log it into the app.
station.has_alarm came back False for us, but the arm mode was still readable via _alarm_data.mode.
Stack
Python 3.12, python-xsense, FastAPI + uvicorn, vanilla-JS frontend (WebSocket, no framework), Docker Compose on Synology (x86_64), floor plan rendered from a PDF via poppler.
@theosnel — genuinely, thank you. Your work took this from "impossible without an official API" to a live, daily-use tool for our team in a single afternoon. If a tip/coffee link exists somewhere, point me to it. And if any of the field notes above are useful to fold back into the docs, happy to help.
A huge thank-you to @theosnel — your python-xsense turned a "nice idea" into a working control-room dashboard
We want to share what became possible thanks to the reverse-engineering work in this thread, and say a proper thank-you.
Using
python-xsensewe built a company-internal, standalone, self-hosted control-room ("Leitwarte") dashboard for the X-Sense security line (door/motion/smoke/water/keypad on an SBS50 base). All 21 sensors show live, and there's a floor-plan map where each sensor sits exactly where it physically hangs, colored by live status. It runs in Docker on our Synology NAS. The whole thing was built in an afternoon together with an AI coding assistant — and the single reason it went that fast is that Theo had already decoded the protocol. That groundwork was a massive relief for us. 🙏The control-room design (the part we're most happy with)
The goal was a single glance = full situational awareness, like a real security desk.
Architecture
How python-xsense is used (for anyone starting out)
AsyncXSense()→init()→login(user, pass)(AWS Cognito user pool, SRP) →load_all().get_house_state(),get_station_state(),get_state(),get_alarm_state().Device types & fields we observed (security line)
Might save the next person some digging:
SDS0AisOpen("1" = open),openRemindSMS0AisMoved("1" = motion),isArmedXS0B-MRalarmStatus,isLifeEndSWS51alarmStatusSKP0AalarmStatusSBS50_alarm_data.mode(Disarmed/Armed)Common fields on every device:
online("1"),batInfo(0–3 battery level),rfLevel(1–3 signal),alarmStatus(bool).Gotchas we hit (hopefully useful)
str | Nonesyntax; it won't import on 3.9.aiohttpis an extra dependency for the async client — install it explicitly.AsyncXSensehas noclose()method — guard your shutdown.SessionExpired: another device is logged in!. Fix: create a dedicated X-Sense account, share the home to it, and use that only for the integration — never log it into the app.station.has_alarmcame backFalsefor us, but the arm mode was still readable via_alarm_data.mode.Stack
Python 3.12,
python-xsense, FastAPI + uvicorn, vanilla-JS frontend (WebSocket, no framework), Docker Compose on Synology (x86_64), floor plan rendered from a PDF via poppler.@theosnel — genuinely, thank you. Your work took this from "impossible without an official API" to a live, daily-use tool for our team in a single afternoon. If a tip/coffee link exists somewhere, point me to it. And if any of the field notes above are useful to fold back into the docs, happy to help.