You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Applies the MODIFIED transition-events requirement from
add-ble-presence-gate (#111) into canonical
openspec/specs/bluetooth-scanning/spec.md. The "No debounce
SHALL be applied" sentence is replaced with the PENDING /
PRESENT / DEPARTED gated semantics + named-bypass +
connected-bypass rules; three new scenarios cover the gate
paths (anonymous-below-gate-is-silent, graduates-after-gate,
gate-zero-restores-no-debounce). Moves the change dir to
openspec/changes/archive/2026-05-21-add-ble-presence-gate/.
All artifacts done, all tasks complete, validate --specs
--strict green (21/21).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: openspec/specs/bluetooth-scanning/spec.md
+27-6Lines changed: 27 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -154,20 +154,41 @@ The exclusion is implemented via the `category_only=True` flag on `service_categ
154
154
-**AND** the BLE detail modal SHALL show `Device Information` in the Services section
155
155
156
156
### Requirement: `BLEPoller` SHALL emit transition events when devices enter and leave its tracked state
157
-
`BLEPoller` SHALL emit `BLEDeviceSeenEvent`the first time a device's `identifier`(the rotation-folded stable id) appears in its tracked state map. `BLEPoller` SHALL emit `BLEDeviceLeftEvent` when a tracked device's `last_seen` falls more than the existing TTL behind the latest snapshot AND the device is then removed from state.
157
+
`BLEPoller` SHALL emit `BLEDeviceSeenEvent`when a device's `identifier`graduates from PENDING to PRESENT in its tracked state map. Graduation happens via one of two paths:
158
158
159
-
No debounce SHALL be applied — every first-seen identifier generates exactly one `BLEDeviceSeenEvent`, even for short-lived ghost MACs that disappear after a single advertisement. Subsequent observations of the same identifier in the same session SHALL NOT re-fire `BLEDeviceSeenEvent`.
159
+
-**Bypass path** — the identifier's first observation carries a non-null `name` OR the identifier comes from the `_connected` snapshot. Graduates to PRESENT on the same tick, `BLEDeviceSeenEvent` fires with the original `first_seen` timestamp.
160
+
-**Gated path** — the identifier's first observation is anonymous (no helper-given `name`, only `vendor` + RSSI). The identifier enters PENDING with a stored `first_seen` timestamp. On each subsequent tick, the poller checks whether `(now - first_seen) >= presence_gate_s`. When that elapses AND the identifier is still in `_devices`, the identifier graduates to PRESENT and `BLEDeviceSeenEvent` fires with `timestamp = first_seen` (NOT the wall-clock graduation time).
161
+
162
+
`presence_gate_s` is configurable via `BLEPoller(presence_gate_s=...)` and defaults to **5.0** seconds. A value of `0.0` restores the pre-gate "every first-seen identifier fires seen on its first observation" behaviour, including for anonymous adverts; in that case PENDING is bypassed entirely.
163
+
164
+
`BLEPoller` SHALL emit `BLEDeviceLeftEvent` when a PRESENT device's `last_seen` falls more than the existing TTL behind the latest snapshot AND the device is then removed from state.
165
+
166
+
If a PENDING identifier is evicted from `_devices` (TTL elapses) before its presence-gate matures, the poller SHALL emit NO transition events for it — no seen, no left. The identifier returns to INIT silently; a future re-appearance from the same identifier opens a fresh PENDING window.
167
+
168
+
Subsequent observations of the same identifier in the same session SHALL NOT re-fire `BLEDeviceSeenEvent`.
160
169
161
170
After a `BLEDeviceLeftEvent` has fired for a given identifier within a session, the poller SHALL emit no further transition events for that identifier in the same session — neither another `BLEDeviceLeftEvent` if the identifier flaps back into `_devices` and is evicted again, nor a fresh `BLEDeviceSeenEvent` if a new advertisement re-introduces it. The identifier is terminal-departed for the rest of the session.
162
171
163
172
The `BLEPoller.events()` async iterator's union return type SHALL include `BLEDeviceSeenEvent` and `BLEDeviceLeftEvent` alongside the existing `BLEScanUpdate`.
164
173
165
-
#### Scenario: First advertisement from a new MAC fires seen
166
-
-**WHEN** an advertisement parses into a BLEDevice whose `identifier` is not in `_state`
167
-
-**THEN**`BLEDeviceSeenEvent` is yielded; on the next observation of the same identifier no further seen event is emitted
174
+
#### Scenario: Named first advert bypasses the presence gate
175
+
-**WHEN** an advertisement parses into a BLEDevice with `name = "Magic Keyboard"`, `vendor = "Apple, Inc."`, `identifier` not in `_state`
176
+
-**THEN**`BLEDeviceSeenEvent` is yielded on the same `_detect_transitions` tick; the identifier moves directly to PRESENT without entering PENDING
177
+
178
+
#### Scenario: Anonymous first advert below the gate is silent
179
+
-**WHEN** an anonymous advertisement (no `name`, only `vendor`) populates `_devices[ident]` at t=0 with default `presence_gate_s = 5.0`, AND the identifier ages out via TTL at t=4
180
+
-**THEN** no `BLEDeviceSeenEvent` is yielded; no `BLEDeviceLeftEvent` is yielded; the identifier leaves `_pending_seen` silently
181
+
182
+
#### Scenario: Anonymous first advert graduates after the gate elapses
183
+
-**WHEN** an anonymous advertisement populates `_devices[ident]` at t=0 with `first_seen = t=0` and `presence_gate_s = 5.0`, AND the device is still in `_devices` at t=5.1 (subsequent adverts kept `last_seen` recent)
184
+
-**THEN**`BLEDeviceSeenEvent` is yielded with `timestamp = t=0` (the original first_seen, NOT wall-clock at graduation); the identifier moves from PENDING to PRESENT
-**WHEN**`BLEPoller(presence_gate_s=0.0)` is constructed AND an anonymous advertisement populates `_devices[ident]` for the first time
188
+
-**THEN**`BLEDeviceSeenEvent` is yielded on the same tick, with no PENDING state entered
168
189
169
190
#### Scenario: TTL eviction fires left
170
-
-**WHEN** a tracked device's `last_seen` exceeds the BLE TTL relative to the latest snapshot's `now`
191
+
-**WHEN** a tracked device's `last_seen` exceeds the BLE TTL relative to the latest snapshot's `now` AND the identifier had previously graduated to PRESENT
171
192
-**THEN**`BLEDeviceLeftEvent` is yielded with `seen_for_seconds = last_seen - first_seen`; the entry is removed from `_state`
172
193
173
194
#### Scenario: Repeated TTL eviction of the same identifier is silent
0 commit comments