143 lines
5.4 KiB
Go
143 lines
5.4 KiB
Go
package platform
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"math"
|
|
"time"
|
|
)
|
|
|
|
const nativeAlarmRuleID = "native-gb32960-alarm"
|
|
|
|
// Require the complete 0x07 unit: absent/invalid data must never clear an alarm.
|
|
func nativeAlarmFields(record AlertStreamRecord) (map[string]json.RawMessage, int, bool, bool) {
|
|
if record.Protocol != "GB32960" {
|
|
return nil, 0, false, false
|
|
}
|
|
fields := make(map[string]json.RawMessage, 6)
|
|
level, levelOK := alertStreamRawNumber(record.Fields["gb32960.alarm.max_alarm_level"])
|
|
flag, flagOK := alertStreamRawNumber(record.Fields["gb32960.alarm.general_alarm_flag"])
|
|
if !levelOK || !flagOK || level < 0 || level > 3 || math.Trunc(level) != level || flag < 0 || flag > math.MaxUint32 || math.Trunc(flag) != flag {
|
|
return nil, 0, false, false
|
|
}
|
|
mask := nativeAlarm2016Mask
|
|
if nativeAlarmVersion(record.Fields) == "V2025" {
|
|
mask = math.MaxUint32
|
|
}
|
|
active := level > 0 || uint32(flag)&mask != 0
|
|
if version := record.Fields["gb32960.header.version"]; len(version) > 0 {
|
|
fields["gb32960.header.version"] = version
|
|
}
|
|
for _, name := range []string{"max_alarm_level", "general_alarm_flag", "battery_faults", "motor_faults", "engine_faults", "other_faults"} {
|
|
key := "gb32960.alarm." + name
|
|
raw := record.Fields[key]
|
|
if name != "max_alarm_level" && name != "general_alarm_flag" {
|
|
// The gateway's FIELDS contract serializes complex KV values as JSON strings.
|
|
var encoded string
|
|
if json.Unmarshal(raw, &encoded) == nil {
|
|
raw = json.RawMessage(encoded)
|
|
}
|
|
var codes []string
|
|
if len(raw) == 0 || string(raw) == "null" || json.Unmarshal(raw, &codes) != nil || codes == nil {
|
|
return nil, 0, false, false
|
|
}
|
|
active = active || len(codes) > 0
|
|
}
|
|
fields[key] = raw
|
|
}
|
|
return fields, int(level), active, true
|
|
}
|
|
|
|
// Runs in the same transaction as the Kafka checkpoint, independently of rules.
|
|
func recordNativeAlarmsTx(ctx context.Context, tx *sql.Tx, records []AlertStreamRecord, result *AlertEvaluationResult) error {
|
|
eligible := make([]AlertStreamRecord, 0)
|
|
for _, record := range records {
|
|
if _, _, _, ok := nativeAlarmFields(record); ok && !record.Late {
|
|
eligible = append(eligible, record)
|
|
}
|
|
}
|
|
if len(eligible) == 0 {
|
|
return nil
|
|
}
|
|
metadata, err := loadAlertStreamVehicleMetadata(ctx, tx, eligible)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, record := range eligible {
|
|
fields, level, active, _ := nativeAlarmFields(record)
|
|
// Serialize concurrent consumers for this VIN, including the first observation.
|
|
if _, err = tx.ExecContext(ctx, `INSERT IGNORE INTO vehicle_native_alarm_state(vin,last_event_at) VALUES(?,'1970-01-01 00:00:00')`, record.VIN); err != nil {
|
|
return err
|
|
}
|
|
var lastAt time.Time
|
|
var eventID string
|
|
if err = tx.QueryRowContext(ctx, `SELECT last_event_at,active_event_id FROM vehicle_native_alarm_state WHERE vin=? FOR UPDATE`, record.VIN).Scan(&lastAt, &eventID); err != nil {
|
|
return err
|
|
}
|
|
if !record.EventAt.After(lastAt) {
|
|
result.LateObservations++
|
|
continue
|
|
}
|
|
if active && eventID == "" {
|
|
eventID, err = newAlertID("alert")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
severity := "minor"
|
|
if level == 2 {
|
|
severity = "major"
|
|
}
|
|
if level == 3 {
|
|
severity = "critical"
|
|
}
|
|
description, _ := DescribeNativeAlarm(fields)
|
|
title := description.Title
|
|
rule := AlertRule{ID: nativeAlarmRuleID, Name: title, Severity: severity, TriggerType: "metric", Metric: "alarm_active", Operator: "eq", Threshold: 1}
|
|
item := alertStreamEvidence(record, metadata[record.VIN], nil)
|
|
shanghai := time.FixedZone("Asia/Shanghai", 8*60*60)
|
|
item.EventAt = record.EventAt.In(shanghai).Format("2006-01-02 15:04:05.999")
|
|
item.ReceivedAt = record.ReceivedAt.In(shanghai).Format("2006-01-02 15:04:05.999")
|
|
query, args := buildAlertEventInsert(eventID, nativeAlarmRuleID+"|"+record.VIN+"|GB32960", rule, item, 1)
|
|
// Missing coordinates must not appear as a real position at (0,0).
|
|
if !item.HasLocation {
|
|
args[17], args[18], args[19] = "", nil, nil
|
|
}
|
|
if _, err = tx.ExecContext(ctx, query, args...); err != nil {
|
|
return err
|
|
}
|
|
payload, marshalErr := json.Marshal(fields)
|
|
if marshalErr != nil {
|
|
return marshalErr
|
|
}
|
|
if _, err = tx.ExecContext(ctx, `INSERT INTO vehicle_native_alarm_evidence(event_id,fields_json) VALUES(?,?)`, eventID, string(payload)); err != nil {
|
|
return err
|
|
}
|
|
if _, err = tx.ExecContext(ctx, `INSERT INTO vehicle_alert_event_action(event_id,action,from_status,to_status,actor,note) VALUES(?,'trigger','','unprocessed','native-gb32960','车辆上报原生告警')`, eventID); err != nil {
|
|
return err
|
|
}
|
|
result.Opened++
|
|
} else if !active && eventID != "" {
|
|
update, updateErr := tx.ExecContext(ctx, `UPDATE vehicle_alert_event SET status='recovered',recovered_at=?,version=version+1 WHERE id=? AND status IN ('unprocessed','processing')`, record.EventAt, eventID)
|
|
if updateErr != nil {
|
|
return updateErr
|
|
}
|
|
count, countErr := update.RowsAffected()
|
|
if countErr != nil {
|
|
return countErr
|
|
}
|
|
if count > 0 {
|
|
if _, err = tx.ExecContext(ctx, `INSERT INTO vehicle_alert_event_action(event_id,action,from_status,to_status,actor,note) VALUES(?,'recover','','recovered','native-gb32960','车辆上报原生告警解除')`, eventID); err != nil {
|
|
return err
|
|
}
|
|
result.Recovered++
|
|
}
|
|
eventID = ""
|
|
}
|
|
if _, err = tx.ExecContext(ctx, `UPDATE vehicle_native_alarm_state SET last_event_at=?,active_event_id=? WHERE vin=?`, record.EventAt, eventID, record.VIN); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|