Get/set the model-free detection sensitivity: the presence floor and the debounce depth the sensing engine uses. These are the RUVIEW_PRESENCE_FLOOR / RUVIEW_DEBOUNCE_FRAMES env vars; POST writes them to a systemd drop-in (ragnar-sensing.service.d/sensitivity.conf) so they survive a RuSe
()
| 1457 | |
| 1458 | try: |
| 1459 | min_conf = float(shared_data.config.get('rusense_notify_min_confidence', 0.95)) |
| 1460 | except (TypeError, ValueError): |
| 1461 | min_conf = 0.95 |
| 1462 | # Confidence only means something with a trained model. Model-less it is |
| 1463 | # uncalibrated (~0.5), so a high bar would silently drop real presence — |
| 1464 | # relax the gate to 0 and lean on the motion-corroborated presence rule + |
| 1465 | # sustain debounce instead. Never miss presence for lack of a model. |
| 1466 | has_model = _rusense_model_active() |
| 1467 | if not has_model: |
| 1468 | min_conf = 0.0 |
| 1469 | # Duty gates are model-aware: a loaded model lifts the empty-room baseline |
| 1470 | # (see the constants above), so use the higher gates then. |
| 1471 | pres_on = _RUSENSE_PRES_ON_MODEL if has_model else _RUSENSE_PRES_ON |
| 1472 | pres_off = _RUSENSE_PRES_OFF_MODEL if has_model else _RUSENSE_PRES_OFF |
| 1473 | try: |
| 1474 | sustain = float(shared_data.config.get('rusense_notify_sustain_s', 2)) |
| 1475 | except (TypeError, ValueError): |
| 1476 | sustain = 2.0 |
| 1477 | try: |
| 1478 | gf_window = int(shared_data.config.get('rusense_geofence_window', 30)) |
| 1479 | except (TypeError, ValueError): |
| 1480 | gf_window = 30 |
| 1481 | gf_window = max(4, min(600, gf_window)) |
| 1482 | gf_enabled = bool(shared_data.config.get('rusense_geofence_enabled', True)) |
| 1483 | now = time.time() |
| 1484 | # Surveillance alerts also respect the permitted-times schedule (if enabled); |
| 1485 | # node-offline + inactivity keep plain notify_on (maintenance / health). |
| 1486 | alerts_now = notify_on and _rusense_within_alert_schedule(now) |
| 1487 | |
| 1488 | # ── Nodes first: the roster feeds BOTH the geofence RSSI windows and the |
| 1489 | # offline detector, so fetch it once and reuse it. ── |
| 1490 | nodes = _rusense_get('/api/v1/nodes') |
| 1491 | node_list = nodes.get('nodes') if isinstance(nodes, dict) else None |
| 1492 | if not isinstance(node_list, list): |
| 1493 | node_list = None |
| 1494 | |
| 1495 | # Extend per-node RSSI windows and evaluate the geofence verdict. A "ghost" |
| 1496 | # is a disturbance with real energy that is NOT corroborated/interior — i.e. |
| 1497 | # a walk-by or through-wall neighbour. When detected we suppress the |
| 1498 | # occupied/motion/people RISE alerts (the "empty" alert is never a ghost). |
| 1499 | block_motion = False |
| 1500 | node_people = None |
| 1501 | if node_list is not None: |
| 1502 | # Per-node person_count is the only count the engine exposes (there is no |
| 1503 | # reliable aggregate). It ghosts on individual nodes, so take the MEDIAN |
| 1504 | # across active nodes — a single node spiking (e.g. node 2 → 3) can't move |
| 1505 | # the median, so a count is only believed when a majority of nodes agree. |
| 1506 | counts = sorted(int(n['person_count']) for n in node_list |
| 1507 | if n.get('status') == 'active' |
| 1508 | and isinstance(n.get('person_count'), (int, float))) |
| 1509 | if counts: |
| 1510 | node_people = counts[(len(counts) - 1) // 2] # lower median (conservative) |
| 1511 | with _rusense_notify_lock: |
| 1512 | windows = _rusense_notify_state['rssi_windows'] |
| 1513 | seen = set() |
| 1514 | for n in node_list: |
| 1515 | nid, rssi = n.get('node_id'), n.get('rssi_dbm') |
| 1516 | if nid is None or rssi is None: |
nothing calls this directly
no test coverage detected