import 'dart:async'; import 'package:dio/dio.dart'; import 'package:logger/logger.dart'; import 'package:rxdart/rxdart.dart'; import '../../domain/entities/alarm_state.dart'; import '../../domain/entities/server_config.dart'; import '../models/bridge_event.dart'; /// Service for querying and tracking alarm state from GeViServer class AlarmService { final Logger _logger = Logger(); final Map _httpClients = {}; final Map _servers = {}; // Alarm state final _alarmsController = BehaviorSubject>.seeded([]); Timer? _syncTimer; /// Stream of active alarms Stream> get alarms => _alarmsController.stream; /// Current active alarms List get currentAlarms => _alarmsController.value; /// Initialize with GeViServer configurations Future initialize(List servers) async { // Only use GeViServer type servers for alarm queries final geviServers = servers.where((s) => s.type == ServerType.geviserver && s.enabled); for (final server in geviServers) { _servers[server.id] = server; _httpClients[server.id] = Dio(BaseOptions( baseUrl: server.bridgeUrl, connectTimeout: const Duration(seconds: 5), receiveTimeout: const Duration(seconds: 10), )); } _logger.i('AlarmService initialized with ${_servers.length} GeViServers'); } /// Query all active alarms from all GeViServers Future> queryAllAlarms() async { final allAlarms = []; for (final entry in _servers.entries) { final serverId = entry.key; try { final alarms = await _queryAlarmsFromServer(serverId); allAlarms.addAll(alarms); _logger.i('Queried ${alarms.length} alarms from $serverId'); } catch (e) { _logger.e('Failed to query alarms from $serverId: $e'); } } _alarmsController.add(allAlarms); return allAlarms; } /// Query alarms from a specific server using GetFirstAlarm/GetNextAlarm pattern Future> _queryAlarmsFromServer(String serverId) async { final client = _httpClients[serverId]; if (client == null) return []; final alarms = []; try { // Try the bulk endpoint first (preferred) final response = await client.get('/alarms/active'); if (response.statusCode == 200) { final data = response.data as Map; final alarmList = data['alarms'] as List? ?? []; for (final alarmJson in alarmList) { final alarm = _parseAlarmFromJson( alarmJson as Map, serverId, ); if (alarm != null) { alarms.add(alarm); } } return alarms; } } catch (e) { _logger.d('Bulk alarm query failed, trying iteration: $e'); } // Fallback to GetFirstAlarm/GetNextAlarm iteration try { // Get first alarm var response = await client.get('/alarms/first'); if (response.statusCode != 200) return alarms; var data = response.data as Map; if (data['alarm'] == null) return alarms; // No alarms var alarm = _parseAlarmFromJson( data['alarm'] as Map, serverId, ); if (alarm != null) alarms.add(alarm); // Iterate through remaining alarms while (true) { response = await client.get('/alarms/next'); if (response.statusCode != 200) break; data = response.data as Map; if (data['alarm'] == null) break; // No more alarms alarm = _parseAlarmFromJson( data['alarm'] as Map, serverId, ); if (alarm != null) alarms.add(alarm); // Safety limit if (alarms.length > 1000) { _logger.w('Alarm query hit safety limit of 1000'); break; } } } catch (e) { _logger.e('Alarm iteration failed: $e'); } return alarms; } /// Parse alarm from JSON response AlarmState? _parseAlarmFromJson(Map json, String serverId) { try { return AlarmState( eventId: json['EventID'] as int? ?? json['event_id'] as int? ?? 0, eventName: json['EventName'] as String? ?? json['event_name'] as String? ?? '', typeId: json['TypeID'] as int? ?? json['type_id'] as int? ?? 0, foreignKey: _parseForeignKey(json['ForeignKey'] ?? json['foreign_key']), serverId: serverId, startedAt: _parseDateTime(json['StartedAt'] ?? json['started_at']), stoppedAt: _parseNullableDateTime(json['StoppedAt'] ?? json['stopped_at']), isActive: json['IsActive'] as bool? ?? json['is_active'] as bool? ?? true, status: AlarmStatus.fromValue( json['Status'] as int? ?? json['status'] as int? ?? 0, ), associatedMonitor: json['AssociatedMonitor'] as int? ?? json['associated_monitor'] as int?, ); } catch (e) { _logger.e('Failed to parse alarm: $e'); return null; } } int _parseForeignKey(dynamic value) { if (value is int) return value; if (value is String) return int.tryParse(value) ?? 0; return 0; } DateTime _parseDateTime(dynamic value) { if (value is DateTime) return value; if (value is String) return DateTime.tryParse(value) ?? DateTime.now(); return DateTime.now(); } DateTime? _parseNullableDateTime(dynamic value) { if (value == null) return null; if (value is DateTime) return value; if (value is String) return DateTime.tryParse(value); return null; } /// Start periodic alarm sync void startPeriodicSync(Duration interval) { stopPeriodicSync(); _syncTimer = Timer.periodic(interval, (_) => queryAllAlarms()); _logger.i('Started periodic alarm sync every ${interval.inSeconds}s'); } /// Stop periodic alarm sync void stopPeriodicSync() { _syncTimer?.cancel(); _syncTimer = null; } /// Handle alarm event from WebSocket void handleAlarmEvent(BridgeEvent event) { final currentAlarms = List.from(_alarmsController.value); if (event.isEventStarted) { // New alarm started final newAlarm = AlarmState( eventId: event.eventId ?? 0, eventName: event.eventName ?? '', typeId: event.typeId ?? 0, foreignKey: event.foreignKey ?? 0, serverId: event.serverId, startedAt: event.timestamp, isActive: true, status: AlarmStatus.newAlarm, ); // Remove existing alarm with same ID if any currentAlarms.removeWhere((a) => a.eventId == newAlarm.eventId && a.serverId == newAlarm.serverId); currentAlarms.add(newAlarm); _logger.d('Alarm started: ${newAlarm.eventName} (ID: ${newAlarm.eventId})'); } else if (event.isEventStopped) { // Alarm stopped final index = currentAlarms.indexWhere((a) => a.eventId == event.eventId && a.serverId == event.serverId); if (index >= 0) { currentAlarms[index] = currentAlarms[index].stopped(); _logger.d('Alarm stopped: ${currentAlarms[index].eventName}'); } } else if (event.isAlarmQueueNotification) { // Alarm status changed (presented, confirmed, etc.) final alarmId = event.alarmId; final notification = event.notification; if (alarmId != null && notification != null) { final index = currentAlarms.indexWhere((a) => a.eventId == alarmId && a.serverId == event.serverId); if (index >= 0) { final newStatus = AlarmStatus.fromValue(notification); currentAlarms[index] = currentAlarms[index].withStatus(newStatus); _logger.d( 'Alarm status changed: ${currentAlarms[index].eventName} -> ${newStatus.name}'); } } } _alarmsController.add(currentAlarms); } /// Get alarms blocking a specific monitor List getAlarmsForMonitor(int monitorId) { return _alarmsController.value .where((a) => a.associatedMonitor == monitorId && a.blocksMonitor) .toList(); } /// Check if any alarm blocks a monitor bool isMonitorBlocked(int monitorId) { return _alarmsController.value .any((a) => a.associatedMonitor == monitorId && a.blocksMonitor); } /// Dispose resources void dispose() { stopPeriodicSync(); _alarmsController.close(); _httpClients.clear(); _servers.clear(); } }