Files
companion/lib/providers.dart

228 lines
8.8 KiB
Dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'domain/models/server_config.dart';
import 'domain/models/sensor.dart';
import 'domain/models/calibration.dart';
import 'features/sensors/calibration_notifier.dart';
import 'domain/models/floor.dart';
import 'domain/models/tag.dart';
import 'domain/models/particle.dart';
import 'domain/models/floor_plan_mode.dart';
import 'data/sources/local/credential_store.dart';
import 'data/sources/localiser/onboarding_client.dart';
import 'data/sources/localiser/session_client.dart';
import 'data/sources/localiser/floor_client.dart';
import 'data/sources/localiser/sensor_client.dart';
import 'data/sources/localiser/tag_client.dart';
import 'data/sources/localiser/realtime_data_client.dart';
import 'data/repositories/onboarding_repository.dart';
import 'data/repositories/sensor_repository.dart';
import 'data/repositories/tag_repository.dart';
import 'data/repositories/floor_plan_repository.dart';
import 'data/repositories/phoenix_onboarding_repository.dart';
import 'data/repositories/phoenix_sensor_repository.dart';
import 'data/repositories/phoenix_tag_repository.dart';
import 'data/repositories/phoenix_floor_plan_repository.dart';
// ---------------------------------------------------------------------------
// Connection / auth state — set imperatively after login
// ---------------------------------------------------------------------------
/// The server the user has selected. Null until a server is chosen.
final serverConfigProvider = StateProvider<ServerConfig?>((ref) => null);
/// JWT returned by /api/session or /api/setup. Null until authenticated.
final authTokenProvider = StateProvider<String?>((ref) => null);
/// Live WebSocket connection. Null until [RealtimeDataClient.connect] succeeds.
final realtimeDataClientProvider =
StateProvider<RealtimeDataClient?>((ref) => null);
// ---------------------------------------------------------------------------
// Convenience
// ---------------------------------------------------------------------------
final credentialStoreProvider = Provider<CredentialStore>((ref) {
return CredentialStore();
});
/// MQTT broker settings stored locally on the device.
/// Null if not yet configured. Set via [CredentialStore.saveMqttBroker].
final mqttBrokerProvider = FutureProvider<({String host, int port})?>(
(ref) => ref.watch(credentialStoreProvider).loadMqttBroker(),
);
// ---------------------------------------------------------------------------
// Feature clients — throw if required state is missing
// ---------------------------------------------------------------------------
ServerConfig _requireConfig(Ref ref) {
final config = ref.watch(serverConfigProvider);
if (config == null) throw StateError('no server selected');
return config;
}
String _requireToken(Ref ref) {
final token = ref.watch(authTokenProvider);
if (token == null) throw StateError('not authenticated');
return token;
}
RealtimeDataClient _requireRealtime(Ref ref) {
final rt = ref.watch(realtimeDataClientProvider);
if (rt == null) throw StateError('realtime not connected');
return rt;
}
final onboardingClientProvider = Provider<OnboardingClient>((ref) {
return OnboardingClient(config: _requireConfig(ref));
});
final sessionClientProvider = Provider<SessionClient>((ref) {
return SessionClient(config: _requireConfig(ref));
});
final floorClientProvider = Provider<FloorClient>((ref) {
return FloorClient(config: _requireConfig(ref), token: _requireToken(ref));
});
final sensorClientProvider = Provider<SensorClient>((ref) {
return SensorClient(config: _requireConfig(ref), token: _requireToken(ref));
});
final tagClientProvider = Provider<TagClient>((ref) {
return TagClient(config: _requireConfig(ref), token: _requireToken(ref));
});
// ---------------------------------------------------------------------------
// Repositories
// ---------------------------------------------------------------------------
final onboardingRepositoryProvider = Provider<OnboardingRepository>((ref) {
return PhoenixOnboardingRepository(
client: ref.watch(onboardingClientProvider));
});
final sensorRepositoryProvider = Provider<SensorRepository>((ref) {
return PhoenixSensorRepository(
client: ref.watch(sensorClientProvider),
realtime: _requireRealtime(ref),
);
});
final tagRepositoryProvider = Provider<TagRepository>((ref) {
return PhoenixTagRepository(
tagClient: ref.watch(tagClientProvider),
realtime: _requireRealtime(ref),
);
});
final floorRepositoryProvider = Provider<FloorRepository>((ref) {
return PhoenixFloorPlanRepository(client: ref.watch(floorClientProvider));
});
// ---------------------------------------------------------------------------
// Floor plan data
// ---------------------------------------------------------------------------
final floorProvider = FutureProvider.autoDispose<Floor?>((ref) =>
ref.watch(floorRepositoryProvider).getFirstFloor());
final roomsProvider = FutureProvider.autoDispose<List<Room>>((ref) async {
final floor = await ref.watch(floorProvider.future);
if (floor == null) return [];
return ref.watch(floorRepositoryProvider).getRooms(floor.id);
});
// ---------------------------------------------------------------------------
// Sensor data
// ---------------------------------------------------------------------------
final sensorsProvider = FutureProvider.autoDispose<List<Sensor>>((ref) {
return ref.watch(sensorRepositoryProvider).getSensors();
});
final sensorProvider =
FutureProvider.autoDispose.family<Sensor, int>((ref, id) {
return ref.watch(sensorRepositoryProvider).getSensor(id);
});
final sensorVersionProvider =
FutureProvider.autoDispose.family<String?, int>((ref, id) {
return ref.watch(sensorRepositoryProvider).getVersion(id);
});
// ---------------------------------------------------------------------------
// Tag data
// ---------------------------------------------------------------------------
final tagsProvider = FutureProvider.autoDispose<List<Tag>>((ref) {
return ref.watch(tagRepositoryProvider).getTags();
});
final tagProvider =
FutureProvider.autoDispose.family<Tag, int>((ref, id) {
return ref.watch(tagRepositoryProvider).getTag(id);
});
// ---------------------------------------------------------------------------
// Cross-tab UI state
// ---------------------------------------------------------------------------
/// Non-null while the user is placing a sensor on the floor plan via the
/// center-dot placement flow. Cleared on Place, Cancel, or back navigation.
final sensorPlacementProvider = StateProvider<Sensor?>((ref) => null);
/// True when placement mode was initiated from the sensors list tab.
/// Used by FloorPlanScreen to decide whether to navigate back on cancel.
final sensorPlacementOriginSensorsProvider = StateProvider<bool>((ref) => false);
final floorPlanModeProvider =
StateProvider<FloorPlanMode>((ref) => FloorPlanMode.view);
// ---------------------------------------------------------------------------
// Live data streams
// ---------------------------------------------------------------------------
/// Raw events pushed by the server's SensorsChannel.
/// Each map has an `event` key: `sensor_announced` or `sensor_enrollment_timeout`.
final sensorsChannelProvider = StreamProvider<Map<String, dynamic>>((ref) {
return ref.watch(sensorRepositoryProvider).sensorEvents();
});
final tagPositionsProvider = StreamProvider<List<TagPosition>>((ref) {
final repo = ref.watch(tagRepositoryProvider);
return repo.watchPositions();
});
/// Accumulated room occupancy: room_id → [tag_id, ...].
/// Updated incrementally as `occupancy_changed` events arrive on rooms:occupancy.
final roomOccupancyProvider = StreamProvider<Map<int, List<String>>>((ref) {
return ref.watch(tagRepositoryProvider).watchRoomOccupancy();
});
/// The tag whose particle cloud is being visualised on the floor plan.
/// Null when no tag is tracked.
final trackedTagProvider = StateProvider<Tag?>((ref) => null);
/// Live particle snapshots for a specific tag (keyed by tag_id string).
final particleSnapshotProvider =
StreamProvider.autoDispose.family<ParticleSnapshot, String>((ref, tagId) {
return ref.watch(tagRepositoryProvider).watchParticleCloud(tagId);
});
// ---------------------------------------------------------------------------
// Calibration state - keyed by sensor DB id
// ---------------------------------------------------------------------------
/// Interactive calibration state machine for a specific sensor.
/// Auto-disposed when no widget is watching (e.g. after the sheet closes).
final calibrationProvider = StateNotifierProvider.autoDispose
.family<CalibrationNotifier, CalibrationState, ({int id, String deviceId})>(
(ref, sensor) => CalibrationNotifier(
sensorId: sensor.id,
sensorDeviceId: sensor.deviceId,
repo: ref.watch(sensorRepositoryProvider),
),
);