Compare commits

...

2 commits

Author SHA1 Message Date
6371967f8d Merge pull request 'fix: parse real Jimi push format (msgType+data)' (#8) from quality-program-2026-04-12 into main
Some checks are pending
Static Analysis / static (push) Waiting to run
Tests / test (push) Waiting to run
2026-04-21 09:10:21 +00:00
David Kiania
636dd2b8b0 fix: parse actual Jimi push format (msgType+data, field name remap)
Some checks failed
Static Analysis / static (push) Waiting to run
Tests / test (push) Waiting to run
Static Analysis / static (pull_request) Has been cancelled
Tests / test (pull_request) Has been cancelled
Diagnostic logging revealed the real Jimi integration push format:
  Content-Type: application/x-www-form-urlencoded
  Body: msgType=jimi.push.device.alarm&data=<URL-encoded JSON>

Differences from docs:
  - data is one JSON object per POST (not a data_list array)
  - alarm uses imei+alarmTime, NOT deviceImei+gateTime

_parse_request now reads form field `data` (falls back to `data_list`) and
JSON-decodes a single object or array. push_alarm handler accepts either
field naming for forward-compat.

Removes diagnostic INFO log now that format is confirmed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 12:10:08 +03:00

View file

@ -109,47 +109,37 @@ async def _parse_request(request: Request) -> tuple[str, list[dict]]:
Some older/configured endpoints may still use form-encoded. This helper Some older/configured endpoints may still use form-encoded. This helper
handles both so each endpoint doesn't need to know which format arrived. handles both so each endpoint doesn't need to know which format arrived.
""" """
content_type = request.headers.get("content-type", "")
body = await request.body() body = await request.body()
# TEMP DIAGNOSTIC: log every push so we can see what Jimi actually sends.
log.info("push %s: content-type=%r body=%.300s",
request.url.path, content_type,
body.decode("utf-8", errors="replace") if body else "<empty>")
if not body: if not body:
return "", [] return "", []
# ── Try JSON body first (integration push format) ────────────────────────── # Jimi integration push format (observed live):
if "application/json" in content_type or body.lstrip()[:1] == b"{": # Content-Type: application/x-www-form-urlencoded
try: # Body: msgType=<topic>&data=<URL-encoded JSON object or array>
payload = json.loads(body) # The `data` field holds a single JSON object per event, not an array.
token = str(payload.get("token", ""))
raw_dl = payload.get("data_list", [])
if isinstance(raw_dl, list):
items = raw_dl[:MAX_ITEMS_PER_POST]
elif isinstance(raw_dl, str):
items = _parse_data_list(raw_dl)
else:
items = []
log.info("push: parsed JSON body — %d items", len(items))
return token, items
except (json.JSONDecodeError, TypeError):
log.warning("push: JSON body parse failed")
# ── Fall back to form-encoded ───────────────────────────────────────────────
try: try:
form = await request.form() form = await request.form()
token = str(form.get("token", ""))
raw_dl = str(form.get("data_list", ""))
items = _parse_data_list(raw_dl) if raw_dl else []
log.info("push: parsed form body — %d items", len(items))
return token, items
except Exception: except Exception:
log.warning("push: form body parse failed", exc_info=True) log.warning("push: form parse failed", exc_info=True)
return "", [] return "", []
token = str(form.get("token", ""))
raw_data = form.get("data") or form.get("data_list") or ""
if not raw_data:
return token, []
try:
parsed = json.loads(raw_data)
except (json.JSONDecodeError, TypeError):
log.warning("push: data JSON parse failed — %.200s", raw_data)
return token, []
items = parsed if isinstance(parsed, list) else [parsed]
if len(items) > MAX_ITEMS_PER_POST:
log.warning("push: truncated %d%d items", len(items), MAX_ITEMS_PER_POST)
items = items[:MAX_ITEMS_PER_POST]
return token, items
def unix_to_ts(v) -> Optional[str]: def unix_to_ts(v) -> Optional[str]:
"""Convert Unix timestamp (seconds or milliseconds) to ISO string.""" """Convert Unix timestamp (seconds or milliseconds) to ISO string."""
@ -351,9 +341,11 @@ async def push_alarm(request: Request):
for item in items: for item in items:
try: try:
cur.execute("SAVEPOINT sp") cur.execute("SAVEPOINT sp")
imei = clean(item.get("deviceImei")) # Jimi integration push uses `imei` + `alarmTime`, NOT the
# `deviceImei` + `gateTime` fields shown in the API docs.
imei = clean(item.get("imei") or item.get("deviceImei"))
alarm_type = clean(item.get("alarmType")) alarm_type = clean(item.get("alarmType"))
alarm_time = clean_ts(item.get("gateTime")) alarm_time = clean_ts(item.get("alarmTime") or item.get("gateTime"))
# [BUG-02] Also guard alarm_type — NULL alarm_type violates NOT NULL constraint. # [BUG-02] Also guard alarm_type — NULL alarm_type violates NOT NULL constraint.
if not imei or not alarm_time or not alarm_type: if not imei or not alarm_time or not alarm_type:
cur.execute("RELEASE SAVEPOINT sp") cur.execute("RELEASE SAVEPOINT sp")