Sample the engine's smoothed-motion (sm) for a few seconds and return its distribution. Used by the guided sensitivity calibration: the UI runs this once with the room EMPTY and once while the user WALKS, then sets the floor in the gap between the two. sm is derived from the classificati
()
| 1529 | if key not in seen: |
| 1530 | del windows[key] |
| 1531 | positions = shared_data.config.get('rusense_node_positions') or {} |
| 1532 | verdict = _rusense_geofence_verdict(positions, windows) if gf_enabled else None |
| 1533 | _rusense_notify_state['last_geofence'] = verdict |
| 1534 | if verdict and verdict.get('ok') and verdict.get('energetic') and not verdict.get('inside_perimeter'): |
| 1535 | block_motion = True |
| 1536 | |
| 1537 | # ── Presence / motion / people, from the latest sensing frame ── |
| 1538 | latest = _rusense_get('/api/v1/sensing/latest') |
| 1539 | if latest: |
| 1540 | cls = latest.get('classification') or {} |
| 1541 | present_agg = bool(cls.get('presence')) |
| 1542 | # A *motionless* body makes the aggregate `presence` boolean flicker, so |
| 1543 | # the per-node classification is a useful still-body signal. BUT a single |
| 1544 | # node false-fires on its own (live: one weak/edge node reads |
| 1545 | # present_still ~4-6% of the time in an EMPTY apartment — a through-wall |
| 1546 | # neighbour or RF noise near that node), and a lone node was enough to |
| 1547 | # confirm presence → phantom alerts. So require a QUORUM: at least two |
| 1548 | # non-stale nodes must agree before the node path confirms presence (with |
| 1549 | # only one node total, fall back to that one). A real occupant perturbs |
| 1550 | # the CSI to multiple nodes and/or lights up the fused aggregate below, |
| 1551 | # so this keeps real detection while killing lone-node phantoms — the |
| 1552 | # same corroboration rule the people-count already uses. See |
| 1553 | # [[rusense-presence-hold]]. |
| 1554 | node_hits = 0 |
| 1555 | node_total = 0 |
| 1556 | for nf in (latest.get('node_features') or []): |
| 1557 | if nf.get('stale'): |
| 1558 | continue |
| 1559 | node_total += 1 |
| 1560 | ncls = nf.get('classification') or {} |
| 1561 | if ncls.get('presence') or str(ncls.get('motion_level') or '').startswith('present'): |
| 1562 | node_hits += 1 |
| 1563 | # Base presence on the engine's MOTION CLASSIFIER, not the raw `presence` |
| 1564 | # boolean (a still subject flickers the boolean; motion_level is steadier). |
| 1565 | motion_level = str(cls.get('motion_level') or '') |
| 1566 | engine_present = motion_level.startswith('present') or motion_level == 'active' |
| 1567 | # Corroborated presence: require TWO independent votes before confirming. |
| 1568 | # The fused aggregate motion classifier is ONE vote; each non-stale node |
| 1569 | # is one vote. This rejects the two live failure modes seen in an EMPTY |
| 1570 | # apartment: (a) a single weak/edge node twitching alone (through-wall |
| 1571 | # neighbour / RF noise), and (b) the aggregate motion heuristic firing on |
| 1572 | # an environmental RF burst — HVAC, fridge, hallway — for 10+ s with NO |
| 1573 | # node agreeing (this alone crossed the duty gate and lingered PRESENT for |
| 1574 | # minutes in a 15-min soak). A real occupant perturbs the CSI to multiple |
| 1575 | # nodes AND/OR the aggregate, so it clears 2 votes easily. With only one |
| 1576 | # node deployed there can't be 2 votes, so fall back to "any signal". |
| 1577 | votes = (1 if engine_present else 0) + node_hits |
| 1578 | if node_total >= 2: |