Skip to content

Commit ba4749f

Browse files
committed
feat: Update DiscoveryManager to handle DiscoveredDevice for lost events
- Changed the type of lost device events from String to DiscoveredDevice in DiscoveryManager and related classes. - Added disposal logic to prevent memory leaks in DefaultDiscoveryManager. - Updated tests to reflect changes in device lost event handling and ensure proper disposal of DiscoveryManager. - Enhanced kernel to handle known device discovery updates and lost device events more effectively. - Introduced new device candidates store to manage newly discovered devices and prevent duplicates. - Implemented provisioning wizard view model to streamline device provisioning process.
1 parent 1e221e7 commit ba4749f

31 files changed

Lines changed: 1269 additions & 451 deletions

client/integration_test/app_test.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Future<Widget> _buildTestApp({IDeviceModuleRegistry? registry}) async {
4545
// Tests
4646
// ---------------------------------------------------------------------------
4747

48+
/*
4849
/// Repeatedly pumps the tester until [finder] is found or the [timeout]
4950
/// expires. This is more reliable than `pumpAndSettle` when the UI shows an
5051
/// indefinite animation (e.g. a loading spinner) that would otherwise keep
@@ -59,6 +60,7 @@ Future<void> _waitFor(WidgetTester tester, Finder finder, {Duration timeout = co
5960
await tester.pump();
6061
expect(tester.any(finder), true, reason: 'Expected $finder to appear within $timeout');
6162
}
63+
*/
6264

6365
void main() {
6466
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

client/lib/app/app.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import '../core/services/devices/device_manager.dart';
2727
import '../core/services/group_manager.dart';
2828
import '../core/services/chore_manager.dart';
2929
import '../core/services/scene_manager.dart';
30+
import '../features/devices/providers/new_device_candidates_store.dart';
3031
import 'package:borneo_app/core/services/devices/device_module_registry.dart';
3132
import 'package:borneo_app/core/config/language_config.dart';
3233

@@ -223,6 +224,11 @@ class _BorneoAppState extends State<BorneoApp> {
223224
dispose: (context, dm) => dm.dispose(),
224225
),
225226

227+
ChangeNotifierProvider<NewDeviceCandidatesStore>(
228+
create: (context) => NewDeviceCandidatesStore(context.read<IDeviceManager>()),
229+
lazy: false,
230+
),
231+
226232
// ChoreManager
227233
Provider<IChoreManager>(
228234
create: (context) => ChoreManagerImpl(

client/lib/core/services/devices/device_manager.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ abstract class IDeviceManager implements IDisposable {
4343
Future<void> unbind(String deviceID);
4444
Future<void> delete(String id, {Transaction? tx, CancellationToken? cancelToken});
4545
Future<void> update(String id, {Transaction? tx, String? name, String? groupID});
46+
Future<void> updateAddress(String id, Uri address, {CancellationToken? cancelToken});
4647
Future<void> moveToGroup(String id, String newGroupID);
4748
Future<bool> isNewDevice(SupportedDeviceDescriptor matched, {Transaction? tx});
4849
Future<DeviceEntity?> singleOrDefaultByFingerprint(String fingerprint, {Transaction? tx});

client/lib/core/services/devices/device_manager_impl.dart

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ final class DeviceManagerImpl extends IDeviceManager {
3737

3838
// event subscriptions
3939
late final StreamSubscription<UnboundDeviceDiscoveredEvent> _unboundDeviceDiscoveredEventSub;
40+
late final StreamSubscription<KnownDeviceDiscoveryUpdatedEvent> _knownDeviceDiscoveryUpdatedEventSub;
4041
late final StreamSubscription<CurrentSceneChangedEvent> _currentSceneChangedEventSub;
4142

4243
// WotThing management
@@ -58,6 +59,9 @@ final class DeviceManagerImpl extends IDeviceManager {
5859
_unboundDeviceDiscoveredEventSub = allDeviceEvents.on<UnboundDeviceDiscoveredEvent>().listen(
5960
_onUnboundDeviceDiscovered,
6061
);
62+
_knownDeviceDiscoveryUpdatedEventSub = allDeviceEvents.on<KnownDeviceDiscoveryUpdatedEvent>().listen(
63+
_onKnownDeviceDiscoveryUpdated,
64+
);
6165

6266
// Listen for scene changes to manage WotThing lifecycle
6367
_currentSceneChangedEventSub = _globalBus.on<CurrentSceneChangedEvent>().listen(_onCurrentSceneChanged);
@@ -105,6 +109,7 @@ final class DeviceManagerImpl extends IDeviceManager {
105109
void dispose() {
106110
if (!_isDisposed) {
107111
_unboundDeviceDiscoveredEventSub.cancel();
112+
_knownDeviceDiscoveryUpdatedEventSub.cancel();
108113
_currentSceneChangedEventSub.cancel();
109114

110115
_disposeAllWotThings();
@@ -191,13 +196,29 @@ final class DeviceManagerImpl extends IDeviceManager {
191196
@override
192197
Future<void> update(String id, {Transaction? tx, String? name, String? groupID}) async {
193198
if (tx == null) {
194-
return await _db.transaction((tx) => _update(id, tx: tx, name: name, groupID: groupID));
199+
await _db.transaction((tx) async {
200+
await _update(id, tx: tx, name: name, groupID: groupID);
201+
});
195202
} else {
196203
await _update(id, tx: tx, name: name, groupID: groupID);
197204
}
198205
}
199206

200-
Future<void> _update(String id, {required Transaction tx, String? name, String? groupID}) async {
207+
@override
208+
Future<void> updateAddress(String id, Uri address, {CancellationToken? cancelToken}) async {
209+
final updatedEntity = await _db
210+
.transaction((tx) => _update(id, tx: tx, address: address))
211+
.asCancellable(cancelToken);
212+
await _refreshKernelRegistration(updatedEntity, cancelToken: cancelToken);
213+
}
214+
215+
Future<DeviceEntity> _update(
216+
String id, {
217+
required Transaction tx,
218+
String? name,
219+
String? groupID,
220+
Uri? address,
221+
}) async {
201222
final store = stringMapStoreFactory.store(StoreNames.devices);
202223
final originalRecord = await store.record(id).get(tx);
203224
if (originalRecord == null) {
@@ -212,10 +233,14 @@ final class DeviceManagerImpl extends IDeviceManager {
212233
if (groupID != null) {
213234
fieldsToUpdate[DeviceEntity.kGroupIDFieldName] = groupID;
214235
}
236+
if (address != null) {
237+
fieldsToUpdate[DeviceEntity.kAddressFieldName] = address.toString();
238+
}
215239

216240
final updatedRecord = await store.record(id).update(tx, fieldsToUpdate);
217241
final updatedEntity = DeviceEntity.fromMap(id, updatedRecord!);
218242
allDeviceEvents.fire(DeviceEntityUpdatedEvent(oldEntity, updatedEntity));
243+
return updatedEntity;
219244
}
220245

221246
Future<bool> _groupExists(Transaction tx, String groupID) async {
@@ -226,15 +251,15 @@ final class DeviceManagerImpl extends IDeviceManager {
226251

227252
@override
228253
Future<void> moveToGroup(String id, String newGroupID) async {
229-
return await _db.transaction((tx) async {
254+
await _db.transaction((tx) async {
230255
// Allow empty string for ungrouped devices
231256
if (newGroupID.isNotEmpty) {
232257
final exists = await _groupExists(tx, newGroupID);
233258
if (!exists) {
234259
throw KeyNotFoundException(message: 'Cannot find group with ID `$newGroupID`');
235260
}
236261
}
237-
return await _update(id, tx: tx, groupID: newGroupID);
262+
await _update(id, tx: tx, groupID: newGroupID);
238263
});
239264
}
240265

@@ -367,22 +392,47 @@ final class DeviceManagerImpl extends IDeviceManager {
367392

368393
return await _db.transaction((tx) async {
369394
final existed = await singleOrDefaultByFingerprint(event.matched.fingerprint, tx: tx);
370-
if (existed != null) {
371-
final updates = <String, dynamic>{};
372-
if (event.matched.address != existed.address) {
373-
updates[DeviceEntity.kAddressFieldName] = event.matched.address.toString();
374-
}
375-
if (updates.isNotEmpty) {
376-
final store = stringMapStoreFactory.store(StoreNames.devices);
377-
final record = store.record(existed.id);
378-
await record.update(tx, updates);
379-
}
380-
} else {
395+
if (existed == null) {
381396
allDeviceEvents.fire(NewDeviceFoundEvent(event.matched));
382397
}
383398
});
384399
}
385400

401+
Future<void> _onKnownDeviceDiscoveryUpdated(KnownDeviceDiscoveryUpdatedEvent event) async {
402+
logger?.i('Known device discovery updated: ${event.device.id} -> ${event.matched.address}');
403+
assert(isInitialized);
404+
405+
if (event.device.address == event.matched.address) {
406+
return;
407+
}
408+
409+
await updateAddress(event.device.id, event.matched.address);
410+
}
411+
412+
Future<void> _refreshKernelRegistration(DeviceEntity updatedEntity, {CancellationToken? cancelToken}) async {
413+
final wasBound = _kernel.boundDevices.any((bound) => bound.device.id == updatedEntity.id);
414+
415+
_kernel.enterHeartbeatBatch();
416+
try {
417+
await _deviceOperLock
418+
.synchronized(() async {
419+
if (wasBound) {
420+
await _kernel.unbind(updatedEntity.id, cancelToken: cancelToken);
421+
}
422+
423+
_kernel.unregisterDevice(updatedEntity.id);
424+
_kernel.registerDevice(BoundDeviceDescriptor(device: updatedEntity, driverID: updatedEntity.driverID));
425+
426+
if (wasBound) {
427+
await _kernel.tryBind(updatedEntity, updatedEntity.driverID, cancelToken: cancelToken);
428+
}
429+
})
430+
.asCancellable(cancelToken);
431+
} finally {
432+
_kernel.exitHeartbeatBatch();
433+
}
434+
}
435+
386436
@override
387437
WotThing getWotThing(String deviceID) {
388438
// Only return WotThings for devices that are already loaded (current scene)

client/lib/core/services/devices/mdns.dart

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,33 +40,39 @@ final class NsdMdnsDiscovery implements IMdnsDiscovery {
4040
switch (event) {
4141
case BonsoirDiscoveryServiceFoundEvent():
4242
{
43-
event.service.resolve(
44-
_discovery.serviceResolver,
45-
); // Should be called when the user wants to connect to this service.
43+
event.service.resolve(_discovery.serviceResolver);
4644
}
4745
break;
4846
case BonsoirDiscoveryServiceResolvedEvent():
4947
{
50-
String host = event.service.host ?? 'UNKNOWN';
51-
final discovered = MdnsDiscoveredDevice(
52-
host: host,
53-
port: event.service.port,
54-
serviceType: event.service.type,
55-
name: event.service.name,
56-
txt: event.service.attributes,
57-
);
58-
_eventBus.fire(FoundDeviceEvent(discovered));
48+
_eventBus.fire(FoundDeviceEvent(_toDiscoveredDevice(event.service)));
5949
}
6050
break;
6151
case BonsoirDiscoveryServiceUpdatedEvent():
52+
{
53+
_eventBus.fire(FoundDeviceEvent(_toDiscoveredDevice(event.service)));
54+
}
6255
break;
6356
case BonsoirDiscoveryServiceLostEvent():
57+
{
58+
_eventBus.fire(LostDeviceEvent(_toDiscoveredDevice(event.service)));
59+
}
6460
break;
6561
default:
6662
break;
6763
}
6864
}
6965

66+
MdnsDiscoveredDevice _toDiscoveredDevice(BonsoirService service) {
67+
return MdnsDiscoveredDevice(
68+
host: service.host ?? 'UNKNOWN',
69+
port: service.port,
70+
serviceType: service.type,
71+
name: service.name,
72+
txt: service.attributes,
73+
);
74+
}
75+
7076
@override
7177
String get serviceType => _serviceType;
7278

client/lib/devices/borneo/lyfi/views/settings_screen.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@ class SettingsScreen extends StatelessWidget {
356356
}
357357
}
358358

359+
/*
359360
Future<void> _showDeleteDialog(BuildContext context, SettingsViewModel vm) async {
360361
final confirmed = await AsyncConfirmationSheet.show(
361362
context,
@@ -370,6 +371,7 @@ class SettingsScreen extends StatelessWidget {
370371
}
371372
});
372373
}
374+
*/
373375

374376
void _showManualFanPowerDialog(BuildContext context, SettingsViewModel vm, int currentValue) {
375377
double tempValue = currentValue.toDouble();
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import 'dart:async';
2+
import 'dart:collection';
3+
4+
import 'package:borneo_app/core/services/devices/device_manager.dart';
5+
import 'package:borneo_app/features/devices/models/events.dart';
6+
import 'package:borneo_kernel_abstractions/models/supported_device_descriptor.dart';
7+
import 'package:flutter/foundation.dart';
8+
9+
class NewDeviceCandidatesStore extends ChangeNotifier {
10+
final IDeviceManager _deviceManager;
11+
final Map<String, SupportedDeviceDescriptor> _candidatesByFingerprint = {};
12+
late final StreamSubscription<NewDeviceFoundEvent> _newDeviceFoundSub;
13+
late final StreamSubscription<NewDeviceEntityAddedEvent> _newDeviceAddedSub;
14+
bool _disposed = false;
15+
16+
NewDeviceCandidatesStore(this._deviceManager) {
17+
_newDeviceFoundSub = _deviceManager.allDeviceEvents.on<NewDeviceFoundEvent>().listen(_onNewDeviceFound);
18+
_newDeviceAddedSub = _deviceManager.allDeviceEvents.on<NewDeviceEntityAddedEvent>().listen(_onNewDeviceAdded);
19+
}
20+
21+
UnmodifiableListView<SupportedDeviceDescriptor> get candidates {
22+
return UnmodifiableListView(_candidatesByFingerprint.values);
23+
}
24+
25+
int get count => _candidatesByFingerprint.length;
26+
27+
SupportedDeviceDescriptor? byFingerprint(String fingerprint) {
28+
return _candidatesByFingerprint[fingerprint];
29+
}
30+
31+
void _onNewDeviceFound(NewDeviceFoundEvent event) {
32+
final previous = _candidatesByFingerprint[event.device.fingerprint];
33+
if (_isSameCandidate(previous, event.device)) {
34+
return;
35+
}
36+
37+
_candidatesByFingerprint[event.device.fingerprint] = event.device;
38+
_notifyIfActive();
39+
}
40+
41+
void _onNewDeviceAdded(NewDeviceEntityAddedEvent event) {
42+
if (_candidatesByFingerprint.remove(event.device.fingerprint) != null) {
43+
_notifyIfActive();
44+
}
45+
}
46+
47+
bool _isSameCandidate(SupportedDeviceDescriptor? left, SupportedDeviceDescriptor right) {
48+
if (left == null) {
49+
return false;
50+
}
51+
52+
return left.fingerprint == right.fingerprint &&
53+
left.address == right.address &&
54+
left.name == right.name &&
55+
left.driverDescriptor.id == right.driverDescriptor.id;
56+
}
57+
58+
void _notifyIfActive() {
59+
if (!_disposed) {
60+
notifyListeners();
61+
}
62+
}
63+
64+
@override
65+
void dispose() {
66+
if (!_disposed) {
67+
_disposed = true;
68+
_newDeviceFoundSub.cancel();
69+
_newDeviceAddedSub.cancel();
70+
}
71+
super.dispose();
72+
}
73+
}

0 commit comments

Comments
 (0)