[bluetooth] Fix BlueZ events stopping after reconnect (no SERVICES_DISCOVERED) (#20950)

#20903 changed onServicesResolved() to call discoverServices() instead of
firing SERVICES_DISCOVERED unconditionally. discoverServices() only notified
when the openHAB-side service list *grew* (getGattServices().size() >
getServices().size()). That list (supportedServices) is only cleared on BlueZ
object removal, so on a normal reconnect BlueZ re-fires ServicesResolved=true
while the list is already fully populated, the "grew" check is false, and
SERVICES_DISCOVERED is never re-fired.

Consumers built on ConnectedBluetoothHandler re-arm their notifications and
polling off onServicesDiscovered() (e.g. grundfosalpha enables notifications
and schedules its read loop there; the generic binding rebuilds channels).
Swallowing the event leaves the Thing ONLINE but silent after every reconnect
- no events are received, matching #20947.

Fire SERVICES_DISCOVERED whenever the GATT is resolved with services present,
not only when the list just grew. Population still happens only when the list
grows, but the notification now always reaches listeners on a resolve. The
event is idempotent for all consumers (generic only adds missing channels;
ConnectedBluetoothHandler/grundfosalpha re-enabling notifications is a no-op
when already notifying).

Fixes #20947

Signed-off-by: Vlad Kolotoff <vkolotoff@pm.me>
This commit is contained in:
Vlad Kolotov
2026-06-14 13:03:26 +02:00
committed by GitHub
parent 6e83787d8c
commit 2a8504cac6
@@ -386,13 +386,13 @@ public class BlueZBluetoothDevice extends BaseBluetoothDevice implements BlueZEv
@Override
public void onServicesResolved(ServicesResolvedEvent event) {
if (event.isResolved()) {
// Populate our service/characteristic list from the now-resolved GATT before notifying
// Populate our service/characteristic list from the now-resolved GATT and notify
// listeners. BlueZ can deliver ServicesResolved=true while our own supportedServices list
// is still empty (e.g. right after a reconnect, before discoverServices() has run); firing
// SERVICES_DISCOVERED then makes listeners (e.g. the generic handler's channel builder) run
// against an empty list and miss every characteristic. discoverServices() populates the
// list and fires SERVICES_DISCOVERED itself once it has added services, so it is the
// correct trigger here. If the services are already known it is a cheap no-op.
// list and fires SERVICES_DISCOVERED once the GATT is resolved with services present -
// including on a reconnect where the list is unchanged - so it is the correct trigger here.
discoverServices();
}
}
@@ -505,6 +505,14 @@ public class BlueZBluetoothDevice extends BaseBluetoothDevice implements BlueZEv
}
addService(service);
}
}
// Notify whenever the GATT is resolved with services present, not only when our list just
// grew. On a reconnect BlueZ re-fires ServicesResolved=true while our supportedServices map
// is already fully populated from the previous connection (it is only cleared on object
// removal), so the "grew" check above is false. Consumers such as ConnectedBluetoothHandler
// re-arm their notifications/polling off SERVICES_DISCOVERED, so swallowing it here leaves the
// Thing online but silent after every reconnect. The notification is idempotent for listeners.
if (!getServices().isEmpty()) {
notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
}
return true;