"""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: battery_raw = (b0 >> 3) & 0x1F mode = (b1 >> 3) & 0x1F 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 & 0x01), } if mode == 1: # Temperature + Humidity temp_raw = (((b1 >> 2) & 0x01) << 8) | b2 result["temperature_c"] = round((temp_raw - 200) / 8, 2) result["humidity_pct"] = round(b3 / 2, 1) elif mode == 2: # Light brightness_raw = (((b1 >> 2) & 0x01) << 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((b1 >> 1) & 0x03, "unknown") result["event_count"] = (b2 << 8) | b3 elif mode == 4: result["vibration_status"] = {0: "no_vibration", 1: "vibration_detected"}.get((b1 >> 1) & 0x03, "unknown") result["event_count"] = (b2 << 8) | b3 elif mode == 5: result["magnet_status"] = {0: "no_magnet", 1: "magnet_detected"}.get((b1 >> 1) & 0x03, "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: 2014} 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))