Flutter web app replacing legacy WPF CCTV surveillance keyboard controller. Includes wall overview, section view with monitor grid, camera input, PTZ control, alarm/lock/sequence BLoCs, and legacy-matching UI styling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
266 lines
8.4 KiB
Dart
266 lines
8.4 KiB
Dart
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<String, Dio> _httpClients = {};
|
|
final Map<String, ServerConfig> _servers = {};
|
|
|
|
// Alarm state
|
|
final _alarmsController = BehaviorSubject<List<AlarmState>>.seeded([]);
|
|
Timer? _syncTimer;
|
|
|
|
/// Stream of active alarms
|
|
Stream<List<AlarmState>> get alarms => _alarmsController.stream;
|
|
|
|
/// Current active alarms
|
|
List<AlarmState> get currentAlarms => _alarmsController.value;
|
|
|
|
/// Initialize with GeViServer configurations
|
|
Future<void> initialize(List<ServerConfig> 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<List<AlarmState>> queryAllAlarms() async {
|
|
final allAlarms = <AlarmState>[];
|
|
|
|
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<List<AlarmState>> _queryAlarmsFromServer(String serverId) async {
|
|
final client = _httpClients[serverId];
|
|
if (client == null) return [];
|
|
|
|
final alarms = <AlarmState>[];
|
|
|
|
try {
|
|
// Try the bulk endpoint first (preferred)
|
|
final response = await client.get('/alarms/active');
|
|
if (response.statusCode == 200) {
|
|
final data = response.data as Map<String, dynamic>;
|
|
final alarmList = data['alarms'] as List<dynamic>? ?? [];
|
|
|
|
for (final alarmJson in alarmList) {
|
|
final alarm = _parseAlarmFromJson(
|
|
alarmJson as Map<String, dynamic>,
|
|
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<String, dynamic>;
|
|
if (data['alarm'] == null) return alarms; // No alarms
|
|
|
|
var alarm = _parseAlarmFromJson(
|
|
data['alarm'] as Map<String, dynamic>,
|
|
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<String, dynamic>;
|
|
if (data['alarm'] == null) break; // No more alarms
|
|
|
|
alarm = _parseAlarmFromJson(
|
|
data['alarm'] as Map<String, dynamic>,
|
|
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<String, dynamic> 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<AlarmState>.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<AlarmState> 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();
|
|
}
|
|
}
|