Files
snek/dashboard/sensit_decoder.py
T
tarcourt 026ec0620d fix: v3 payload byte 1 layout — 10-bit sensor data, button on bit 2
Sens'it Discovery v3 byte 1 layout per official spec:
  bits 7-3: mode
  bit 2   : special / button alert
  bits 1-0: MSB (2 bits) of sensor data

Previous decoder had button on bit 0 and used only bit 2 as data MSB
(9-bit values). Real payloads confirmed 10-bit: e.g. b609a861 was
decoded as -4°C but correctly resolves to 28°C with the fixed layout.

Fixes applied consistently to all v3 modes:
- Temperature: 10-bit temp with formula (raw - 200) / 8
- Light: 10-bit brightness with formula raw / 96
- Door/Vibration/Magnet: 2-bit status from bits 1-0

Existing DB entries re-decoded in place.
2026-07-06 19:32:46 +02:00

140 lines
4.8 KiB
Python

"""Sens'it payload decoder — supports both v2 and Discovery (v3).
Auto-detects the device generation by inspecting the reserved bits of byte 0:
- v3 Discovery: byte 0 bits 2-0 == 0b110 (fixed marker)
- v2 : otherwise
"""
# --- Discovery v3 (Sens'it 3.1.0+) ---------------------------------------------
V3_MODES = {
0: "Standby / Button",
1: "Temperature",
2: "Light",
3: "Door",
4: "Vibration",
5: "Magnet",
}
def _decode_v3(b0: int, b1: int, b2: int, b3: int) -> dict:
# byte 1 layout per Sens'it Discovery v3 spec:
# bits 7-3: mode
# bit 2 : special / button alert
# bits 1-0: 2 MSB bits of sensor data (giving 10-bit values with b2)
battery_raw = (b0 >> 3) & 0x1F
mode = (b1 >> 3) & 0x1F
data_msb = b1 & 0x03
result = {
"format": "sensit_v3",
"battery": round(battery_raw * 0.05 + 2.7, 2),
"battery_raw": battery_raw,
"reserved_ok": True,
"mode": V3_MODES.get(mode, f"Unknown ({mode})"),
"mode_id": mode,
"button_alert": bool((b1 >> 2) & 0x01),
}
if mode == 1: # Temperature + Humidity (10-bit temp)
temp_raw = (data_msb << 8) | b2
result["temperature_c"] = round((temp_raw - 200) / 8, 2)
result["humidity_pct"] = round(b3 / 2, 1)
elif mode == 2: # Light (10-bit brightness)
brightness_raw = (data_msb << 8) | b2
result["brightness_lux"] = round(brightness_raw / 96, 2)
elif mode == 3:
result["door_status"] = {0: "closed", 1: "opened", 2: "opening_alert", 3: "closing_alert"}.get(data_msb, "unknown")
result["event_count"] = (b2 << 8) | b3
elif mode == 4:
result["vibration_status"] = {0: "no_vibration", 1: "vibration_detected"}.get(data_msb, "unknown")
result["event_count"] = (b2 << 8) | b3
elif mode == 5:
result["magnet_status"] = {0: "no_magnet", 1: "magnet_detected"}.get(data_msb, "unknown")
result["event_count"] = (b2 << 8) | b3
return result
# --- Sens'it v2 ----------------------------------------------------------------
V2_MODES = {
0: "Button",
1: "Temperature",
2: "Light",
3: "Door",
4: "Move",
5: "Reed Switch",
}
V2_TIMEFRAME = {0: "10 min", 1: "1 h", 2: "6 days", 3: "24 h"}
V2_TYPE = {0: "regular", 1: "button_call", 2: "alert", 3: "new_mode"}
V2_LIGHT_MULT = {0: 1, 1: 8, 2: 64, 3: 512}
def _decode_v2(b0: int, b1: int, b2: int, b3: int) -> dict:
mode = b0 & 0x07
timeframe = (b0 >> 3) & 0x03
msg_type = (b0 >> 5) & 0x03
battery_msb = (b0 >> 7) & 0x01
temp_msb = b1 & 0x0F
battery_lsb = (b1 >> 4) & 0x0F
battery_raw = (battery_msb << 4) | battery_lsb
result = {
"format": "sensit_v2",
"battery": round(battery_raw * 0.05 + 2.7, 2),
"battery_raw": battery_raw,
"mode": V2_MODES.get(mode, f"Unknown ({mode})"),
"mode_id": mode,
"timeframe": V2_TIMEFRAME.get(timeframe, str(timeframe)),
"type": V2_TYPE.get(msg_type, str(msg_type)),
"button_alert": msg_type == 1,
}
if mode == 2: # Light — byte 3 not used for temp
light_value = b2 & 0x3F
light_mult = V2_LIGHT_MULT[(b2 >> 6) & 0x03]
result["brightness_lux"] = round(light_mult * light_value * 0.01, 2)
elif mode == 3: # Door — byte 3 reserved for config
pass
else:
# Classic modes: byte 2 = T° LSB (6 bits) + reed switch (1 bit) + unused
temp_lsb = b2 & 0x3F
temp_raw = (temp_msb << 6) | temp_lsb
result["temperature_c"] = round((temp_raw - 200) / 8, 2)
result["reed_switch"] = bool((b2 >> 6) & 0x01)
# Byte 3 depends on mode (per v2 spec)
if mode == 0: # Button — software version (fw major.minor)
result["fw_major"] = (b3 >> 4) & 0x0F
result["fw_minor"] = b3 & 0x0F
elif mode == 1: # Temperature — humidity percentage
result["humidity_pct"] = round(b3 * 0.5, 1)
else: # Light, Door, Move, Reed Switch — number of alerts / events
result["event_count"] = b3
return result
# --- Entry point ---------------------------------------------------------------
def decode(payload_hex: str) -> dict:
"""
Decode a 4-byte Sens'it payload. Auto-detects v2 vs Discovery v3.
"""
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)}
result = (_decode_v3 if (b0 & 0x07) == 0b110 else _decode_v2)(b0, b1, b2, b3)
result["raw"] = payload_hex
return result
if __name__ == "__main__":
for h in ("b60dc86e", "a60dc271", "a87611db", "a8750adb", "a87407db"):
print(h, "", decode(h))