Pure per-device advertisement cache with emit-gating and a bounded size.
No I/O — Bluez.Client owns the side effects (it emits the
adverts this module returns).
Each device path maps to %{props, last_raw, last_emit, last_seen}. On
upsert/4 the new props are merged, the advert is reconstructed
(Bluez.Advert), and an advert is returned to emit only when:
- it's the first sighting, or
- the reconstructed AD payload changed (sensor data update), or
- the heartbeat interval has elapsed (keeps RSSI/last-seen fresh in HA without forwarding every PDU).
The heartbeat is configurable per cache instance (new/1's
heartbeat_ms: opt) because it's the only refresh some devices ever get:
a static-payload beacon (fixed iBeacon UUID/major/minor) changes no AD
bytes ever, so without the heartbeat its RSSI/last-seen would freeze at
first sight for as long as it keeps advertising.
The map is capped at @max_devices (LRU by last_seen) so a device spraying
randomized MACs — each a new BlueZ object path — can't grow it without bound
(BlueZ's InterfacesRemoved is the only other prune and is outside our
control).
Summary
Functions
The default heartbeat interval (ms) used when new/1 is called without heartbeat_ms:.
Emit decision (pure). Emit on first sight (last_raw == nil), on a payload
change, or once the heartbeat interval has elapsed.
An empty cache. heartbeat_ms: (default 10000)
sets how long an unchanged advert is suppressed before upsert/4
re-emits it anyway — see the moduledoc. Raises ArgumentError unless
it's a positive integer.
Drop path's entry (BlueZ emitted InterfacesRemoved for it).
Distinct devices seen within the last window_ms of monotonic now_ms —
the web tab's "devices (15 min)" stat. A plain scan bounded by the LRU
cap, so it's cheap enough to run per stats tick.
Number of device entries currently cached.
Merge new_props for path at monotonic now_ms. Returns
{cache, adverts} where adverts is [] or a single reconstructed advert
the caller should emit.
Types
@type t() :: %Bluez.DeviceCache{ devices: %{optional(String.t()) => map()}, heartbeat_ms: pos_integer() }
Functions
@spec default_heartbeat_ms() :: pos_integer()
The default heartbeat interval (ms) used when new/1 is called without heartbeat_ms:.
Emit decision (pure). Emit on first sight (last_raw == nil), on a payload
change, or once the heartbeat interval has elapsed.
An empty cache. heartbeat_ms: (default 10000)
sets how long an unchanged advert is suppressed before upsert/4
re-emits it anyway — see the moduledoc. Raises ArgumentError unless
it's a positive integer.
Drop path's entry (BlueZ emitted InterfacesRemoved for it).
@spec seen_within(t(), integer(), pos_integer()) :: non_neg_integer()
Distinct devices seen within the last window_ms of monotonic now_ms —
the web tab's "devices (15 min)" stat. A plain scan bounded by the LRU
cap, so it's cheap enough to run per stats tick.
Note the cap also bounds the answer: with > 512 active
devices the count saturates at the cap (eviction forgets the oldest).
@spec size(t()) :: non_neg_integer()
Number of device entries currently cached.
Merge new_props for path at monotonic now_ms. Returns
{cache, adverts} where adverts is [] or a single reconstructed advert
the caller should emit.