# `Bluez.Client`
[🔗](https://github.com/bbangert/bluez/blob/v0.1.0/lib/bluez/client.ex#L1)

Persistent `rebus` D-Bus client + service to `org.bluez`, driving BLE
scanning and turning device signals into advertisements, fanned out
through the `on_advertisement:` fun.

Supports both scanner modes Home Assistant can request (`set_mode/1`,
called via `BluetoothScanner.set_scanner_mode/1`):

  * `:passive` (default) — a BlueZ `AdvertisementMonitor`. We never send
    scan requests, so scannable peripherals don't burn battery answering
    us. Requires *exporting* a D-Bus object BlueZ calls back into, so this
    process is both a client and a service (via the forked rebus's
    `set_method_handler/2`).
  * `:active` — `Adapter1.StartDiscovery` with an LE filter. BlueZ sends
    scan requests, so SCAN_RSP data (e.g. device names) is collected —
    parity with ESP32 proxies' active mode.

Device data arrives the same way in both modes
(`InterfacesAdded`/`PropertiesChanged` on `Device1` objects), so the
advert pipeline downstream is mode-agnostic.

Flow:

  1. `Bluez.Rebus.connect(:system)`, `set_method_handler(self())`, monitor the
     connection, and install bus match rules for org.bluez device signals.
  2. Power the adapter on, then engage `configured_mode/0`: either
     `AdvertisementMonitorManager1.RegisterMonitor` our root object (BlueZ
     enumerates the monitor via `ObjectManager.GetManagedObjects` and calls
     `Activate`/`DeviceFound` on it; the monitor's `or_patterns` (FLAGS
     \x02/\x06/\x1a) match effectively all advertisers — the habluetooth
     "match all" recipe), or `SetDiscoveryFilter` + `StartDiscovery`.
  3. Matched devices surface as `InterfacesAdded`/`PropertiesChanged` signals;
     props are unwrapped (`Bluez.Variant`) and fed to
     `Bluez.DeviceCache`, which reconstructs + emit-gates and
     returns the adverts to fan out via `BluetoothScanner.on_advertisement/1`.

Mode transitions:

  * Run in a Task — BlueZ calls `GetManagedObjects` back on us before
    `RegisterMonitor` returns, so the GenServer must stay free to answer —
    and are serialized: at most one in flight, identified by a generation
    ref so a stale Task result can't corrupt state. A `set_mode/1` arriving
    mid-transition parks in a one-slot pending queue keyed by target mode:
    callers asking for the same target coalesce (all get `:ok` when it
    lands); a different target displaces them with `{:error, :superseded}`
    (latest target wins).
  * Transitions engage the new mode BEFORE disengaging the old one
    (monitor and discovery can legally coexist in BlueZ): a failed engage
    leaves the previous mode still scanning rather than going dark.
    Disengage is best-effort, and self-healing lives in engage's
    idempotency: whatever drifted, re-engaging treats
    `AlreadyExists`/`InProgress` as success, so the next transition always
    converges on the target mode.
  * The configured mode persists in `:persistent_term` across Client
    restarts: a bluetoothd/connection crash re-engages what HA chose
    rather than silently reverting to passive.

Resilience:

  * The rebus connection is monitored; if it dies (e.g. a malformed bus
    frame `:stop`s it) the Client stops and the supervisor restarts it,
    re-establishing the connection.
  * Setup retries via `send_after` (not `Process.sleep`) so the GenServer
    stays responsive while waiting for `bluetoothd` to claim org.bluez.

# `adapters_info`

```elixir
@spec adapters_info() :: [
  %{
    path: String.t(),
    address: String.t() | nil,
    name: String.t() | nil,
    powered: boolean()
  }
]
```

`org.bluez.Adapter1` properties for every adapter object the daemon
exposes: `[%{path:, address:, name:, powered:}]` — e.g. for a host's
radio list. Returns `[]` when this Client isn't running (BT subtree
down, host tests) or the daemon can't answer.

# `adapters_topic`

```elixir
@spec adapters_topic() :: String.t()
```

PubSub topic carrying `{:bluetooth_adapters_changed}` on adapter add/remove.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `configured_mode`

```elixir
@spec configured_mode() :: :passive | :active
```

The HA-configured scanner mode (`:passive` default). Pure
`:persistent_term` read — safe on any target, with or without the Client
running (host tests, early boot).

# `devices_seen`

```elixir
@spec devices_seen(pos_integer()) :: non_neg_integer()
```

Distinct devices the advert cache has seen in the last `window_ms`
(`Bluez.DeviceCache.seen_within/3`). `0` when this Client
isn't running — e.g. for a host's stats ticker.

# `resume_scan`

```elixir
@spec resume_scan() :: :ok
```

Re-engage the HA-configured scanner mode after `suspend_scan/0`.

# `set_mode`

```elixir
@spec set_mode(:passive | :active) :: :ok | {:error, term()}
```

Switch the scanner between `:passive` (AdvertisementMonitor) and `:active`
(StartDiscovery) at runtime. Returns once the BlueZ transition completes.

A caller whose transition is already in flight always gets that
transition's own result. Only callers parked *behind* an in-flight
transition can get `{:error, :superseded}` — when a newer `set_mode/1`
asking for a different mode displaces them (same-mode callers coalesce
and succeed together).

Callers must `catch :exit` for the not-running/timeout cases (see the
moduledoc idiom note in `Bluez`).

# `start_link`

# `suspend_scan`

```elixir
@spec suspend_scan() :: :ok
```

Suspend scanning entirely (disengage the monitor/discovery), preserving the
HA-configured mode so `resume_scan/0` restores it. Used by Improv Wi-Fi
provisioning: it only runs on a no-connectivity boot, when there is no HA
client to consume proxied advertisements anyway — so scanning is pointless
(and may degrade the active BLE peripheral connection on a single radio).

Fire-and-forget cast (the transition runs off-loop); safe to call when the
Client isn't running (no-op).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
