170 lines
7.9 KiB
TypeScript
170 lines
7.9 KiB
TypeScript
import type { AlertEvent, AlertRule, AlertStatus, AlertTriggerType } from '../../api/types';
|
|
import { formatShanghaiDateTime } from './formatters';
|
|
|
|
export type EventCategory = 'safety' | 'geofence' | 'connectivity' | 'telemetry' | 'business';
|
|
export type EventExecutionState = 'attention' | 'handling' | 'automated' | 'completed' | 'ignored';
|
|
|
|
export interface ProtocolEventSource {
|
|
protocol: 'GB32960' | 'JT808' | 'YUTONG_MQTT';
|
|
label: string;
|
|
contract: string;
|
|
role: string;
|
|
}
|
|
|
|
export interface VehicleEventView {
|
|
id: string;
|
|
type: string;
|
|
category: EventCategory;
|
|
categoryLabel: string;
|
|
title: string;
|
|
subject: { type: 'vehicle'; vin: string; plate: string };
|
|
source: { protocol: string; eventId: string };
|
|
occurredAt: string;
|
|
receivedAt: string;
|
|
automation: { id: string; name: string; version: number };
|
|
severity?: AlertEvent['severity'];
|
|
execution: { state: EventExecutionState; label: string; requiresAttention: boolean };
|
|
}
|
|
|
|
export const protocolEventSources: ProtocolEventSource[] = [
|
|
{ protocol: 'GB32960', label: 'GB/T 32960', contract: 'vehicle.telemetry.reported', role: '整车与新能源遥测' },
|
|
{ protocol: 'JT808', label: 'JT/T 808', contract: 'vehicle.location.reported', role: '位置、行驶与终端状态' },
|
|
{ protocol: 'YUTONG_MQTT', label: '宇通 MQTT', contract: 'vehicle.oem.telemetry.reported', role: '厂商扩展遥测' }
|
|
];
|
|
|
|
export const eventCategoryLabels: Record<EventCategory, string> = {
|
|
safety: '安全异常',
|
|
geofence: '地理围栏',
|
|
connectivity: '连接状态',
|
|
telemetry: '遥测变化',
|
|
business: '业务事件'
|
|
};
|
|
|
|
export const eventExecutionLabels: Record<EventExecutionState, string> = {
|
|
attention: '待处理',
|
|
handling: '处理中',
|
|
automated: '已恢复',
|
|
completed: '已完成',
|
|
ignored: '已忽略'
|
|
};
|
|
|
|
const statusToExecution: Record<AlertStatus, EventExecutionState> = {
|
|
unprocessed: 'attention',
|
|
processing: 'handling',
|
|
recovered: 'automated',
|
|
closed: 'completed',
|
|
ignored: 'ignored'
|
|
};
|
|
|
|
function eventCategory(event: Pick<AlertEvent, 'triggerType' | 'metric'>): EventCategory {
|
|
if (event.triggerType === 'geofence') return 'geofence';
|
|
if (event.triggerType === 'offline' || event.metric === 'freshness_sec') return 'connectivity';
|
|
if (event.metric === 'alarm_active' || event.metric.includes('hydrogen')) return 'safety';
|
|
if (event.metric.includes('mileage') || event.metric.includes('daily_')) return 'business';
|
|
return 'telemetry';
|
|
}
|
|
|
|
function normalizedEventType(triggerType: AlertTriggerType | undefined, metric: string, category: EventCategory) {
|
|
if (triggerType === 'geofence') return 'vehicle.geofence.changed';
|
|
if (triggerType === 'offline' || metric === 'freshness_sec') return 'vehicle.connectivity.offline';
|
|
if (triggerType === 'stationary') return 'vehicle.motion.stationary';
|
|
const detail = (metric || 'changed').replace(/[^a-z0-9_]+/gi, '_').toLowerCase();
|
|
return category === 'business' ? `vehicle.business.${detail}` : `vehicle.${category}.${detail}`;
|
|
}
|
|
|
|
export function automationEventType(rule: Pick<AlertRule, 'triggerType' | 'metric' | 'operator'>) {
|
|
if (rule.triggerType === 'geofence') return `vehicle.geofence.${({ enter: 'entered', exit: 'exited', inside: 'inside', outside: 'outside' } as Record<string, string>)[rule.operator] || 'changed'}`;
|
|
if (rule.triggerType === 'offline' || rule.metric === 'freshness_sec') return 'vehicle.connectivity.offline';
|
|
if (rule.triggerType === 'stationary') return 'vehicle.motion.stationary';
|
|
if (rule.metric === 'soc_percent' && ['lt', 'lte'].includes(rule.operator)) return 'vehicle.telemetry.soc_low';
|
|
if (rule.metric === 'speed_kmh' && ['gt', 'gte'].includes(rule.operator)) return 'vehicle.motion.speed_high';
|
|
if (rule.metric === 'alarm_active') return 'vehicle.safety.alarm_activated';
|
|
if (rule.metric === 'hydrogen_concentration_percent') return 'vehicle.safety.hydrogen_concentration_high';
|
|
if (rule.metric === 'daily_mileage_km') return 'vehicle.mileage.daily_completed';
|
|
return normalizedEventType(rule.triggerType, rule.metric, eventCategory({ triggerType: rule.triggerType, metric: rule.metric }));
|
|
}
|
|
|
|
export function toVehicleEvent(event: AlertEvent): VehicleEventView {
|
|
const category = eventCategory(event);
|
|
const state = statusToExecution[event.status];
|
|
return {
|
|
id: event.id,
|
|
type: event.eventType || normalizedEventType(event.triggerType, event.metric, category),
|
|
category: (event.eventCategory as EventCategory | undefined) || category,
|
|
categoryLabel: eventCategoryLabels[(event.eventCategory as EventCategory | undefined) || category],
|
|
title: event.ruleName,
|
|
subject: { type: 'vehicle', vin: event.vin, plate: event.plate },
|
|
source: { protocol: event.protocol, eventId: event.sourceEventId },
|
|
occurredAt: event.eventAt || event.triggeredAt,
|
|
receivedAt: event.receivedAt,
|
|
automation: { id: event.ruleId, name: event.ruleName, version: event.ruleVersion },
|
|
severity: category === 'business' ? undefined : event.severity,
|
|
execution: { state, label: eventExecutionLabels[state], requiresAttention: state === 'attention' || state === 'handling' }
|
|
};
|
|
}
|
|
|
|
export function eventContract(event: AlertEvent) {
|
|
const normalized = toVehicleEvent(event);
|
|
return [
|
|
{ key: 'event.type', value: normalized.type },
|
|
{ key: 'source.protocol', value: normalized.source.protocol },
|
|
{ key: 'subject.vin', value: normalized.subject.vin },
|
|
{ key: 'occurred_at', value: normalized.occurredAt },
|
|
{ key: 'received_at', value: normalized.receivedAt }
|
|
];
|
|
}
|
|
|
|
function shanghaiMinute(value: Date) {
|
|
return formatShanghaiDateTime(value.toISOString()).replace(' ', 'T').slice(0, 16);
|
|
}
|
|
|
|
export function eventEvidenceHistoryPath(event: AlertEvent, windowMinutes = 3) {
|
|
const normalized = toVehicleEvent(event);
|
|
const params = new URLSearchParams({
|
|
keywords: normalized.subject.vin,
|
|
category: 'raw',
|
|
eventId: normalized.id,
|
|
eventTitle: normalized.title,
|
|
eventVin: normalized.subject.vin,
|
|
eventAt: normalized.occurredAt
|
|
});
|
|
if (normalized.subject.plate) params.set('eventPlate', normalized.subject.plate);
|
|
if (normalized.source.protocol) params.set('protocol', normalized.source.protocol);
|
|
const occurredAt = new Date(normalized.occurredAt);
|
|
if (Number.isFinite(occurredAt.getTime())) {
|
|
const safeWindow = Math.min(60, Math.max(1, Math.round(windowMinutes)));
|
|
params.set('dateFrom', shanghaiMinute(new Date(occurredAt.getTime() - safeWindow * 60_000)));
|
|
params.set('dateTo', shanghaiMinute(new Date(occurredAt.getTime() + safeWindow * 60_000)));
|
|
}
|
|
return `/history?${params.toString()}`;
|
|
}
|
|
|
|
export function automationSource(rule: Pick<AlertRule, 'scopeProtocols'>) {
|
|
const protocols = rule.scopeProtocols ?? [];
|
|
if (!protocols.length) return '三类协议';
|
|
if (protocols.length === 1) return protocolEventSources.find((item) => item.protocol === protocols[0])?.label ?? protocols[0];
|
|
return `${protocols.length} 类协议`;
|
|
}
|
|
|
|
export function isNativeAlarm(event: Pick<AlertEvent, 'ruleId'>) {
|
|
return event.ruleId === 'native-gb32960-alarm';
|
|
}
|
|
|
|
export function nativeAlarmDetails(event: AlertEvent) {
|
|
const fields = event.nativeAlarmFields;
|
|
if (!fields) return [];
|
|
return [
|
|
['max_alarm_level', '最高报警等级'],
|
|
['general_alarm_flag', '通用报警标志'],
|
|
['battery_faults', '可充电储能装置故障码'],
|
|
['motor_faults', '驱动电机故障码'],
|
|
['engine_faults', '发动机故障码'],
|
|
['other_faults', '其他故障码'],
|
|
].map(([name, label]) => {
|
|
const value = fields[`gb32960.alarm.${name}`];
|
|
let text = value == null ? '未上报' : Array.isArray(value) ? (value.length ? value.join('、') : '无') : String(value);
|
|
if (name === 'max_alarm_level' && value != null) text = ({ '0': '0 · 无故障', '1': '1 · 一级故障', '2': '2 · 二级故障', '3': '3 · 三级故障', '254': '254 · 异常', '255': '255 · 无效' } as Record<string, string>)[String(value)] ?? text;
|
|
return { key: label, value: text };
|
|
});
|
|
}
|