Initial commit: COPILOT D6 Flutter keyboard controller
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>
This commit is contained in:
265
copilot_keyboard/lib/data/services/alarm_service.dart
Normal file
265
copilot_keyboard/lib/data/services/alarm_service.dart
Normal file
@@ -0,0 +1,265 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user