Bluez.DeviceCache (bluez v0.1.0)

Copy Markdown View Source

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 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

Emit decision (pure). Emit on first sight (last_raw == nil), on a payload change, or once the heartbeat interval has elapsed.

An empty cache.

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

t()

@type t() :: %Bluez.DeviceCache{devices: %{optional(String.t()) => map()}}

Functions

emit?(raw, last_raw, last_emit, now, heartbeat_ms)

@spec emit?(binary(), binary() | nil, integer() | nil, integer(), integer()) ::
  boolean()

Emit decision (pure). Emit on first sight (last_raw == nil), on a payload change, or once the heartbeat interval has elapsed.

new()

@spec new() :: t()

An empty cache.

remove(cache, path)

@spec remove(t(), String.t()) :: t()

Drop path's entry (BlueZ emitted InterfacesRemoved for it).

seen_within(device_cache, now_ms, window_ms)

@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).

size(device_cache)

@spec size(t()) :: non_neg_integer()

Number of device entries currently cached.

upsert(cache, path, new_props, now_ms)

@spec upsert(t(), String.t(), map(), integer()) :: {t(), [Bluez.Advert.advert()]}

Merge new_props for path at monotonic now_ms. Returns {cache, adverts} where adverts is [] or a single reconstructed advert the caller should emit.