From b81d6afecb1768a827b445de624ceebaee0a3f74 Mon Sep 17 00:00:00 2001 From: Hein Date: Tue, 25 Feb 2025 14:31:25 +0200 Subject: [PATCH] Added /hikvision/alarm --- cmd/apisrv/main.go | 421 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 366 insertions(+), 55 deletions(-) diff --git a/cmd/apisrv/main.go b/cmd/apisrv/main.go index 3046d0e..8210bcb 100644 --- a/cmd/apisrv/main.go +++ b/cmd/apisrv/main.go @@ -3,12 +3,14 @@ package main import ( "bytes" "encoding/json" + "encoding/xml" "fmt" "io" "log" "net/http" "net/url" "os" + "strings" "time" ) @@ -22,6 +24,9 @@ type Config struct { TelegramEnabled bool `json:"telegram_enabled"` TelegramToken string `json:"telegram_token"` TelegramChatID string `json:"telegram_chat_id"` + HikEnabled bool `json:"hik_enabled"` + HikUsername string `json:"hik_username"` + HikPassword string `json:"hik_password"` } // VivotekEvent represents the event data structure from Vivotek NVR @@ -34,6 +39,34 @@ type VivotekEvent struct { // Add more fields as needed based on Vivotek's event structure } +// HikVisionEvent represents the alarm data structure from HIKVision +type HikVisionEvent struct { + EventType string `json:"eventType"` + EventTime time.Time `json:"eventTime"` + DeviceID string `json:"deviceId"` + ChannelID string `json:"channelId"` + EventDetails map[string]interface{} `json:"eventDetails"` + // Raw XML data for debugging/logging + RawXML string `json:"-"` +} + +// HIKVisionAlarm represents the XML structure of a HIKVision alarm event +type HIKVisionAlarm struct { + XMLName xml.Name `xml:"EventNotificationAlert"` + IPAddress string `xml:"ipAddress"` + PortNo int `xml:"portNo"` + ProtocolType string `xml:"protocolType"` + MacAddress string `xml:"macAddress"` + ChannelID int `xml:"channelID"` + DateTime string `xml:"dateTime"` + ActivePostCount int `xml:"activePostCount"` + EventType string `xml:"eventType"` + EventState string `xml:"eventState"` + EventDescription string `xml:"eventDescription"` + // Optional fields that may be present in some events + DetectionRegionID int `xml:"detectionRegionID,omitempty"` +} + // GlobalState maintains the application state type GlobalState struct { Config Config @@ -42,13 +75,14 @@ type GlobalState struct { } var state GlobalState +var startTime time.Time // initConfig loads configuration from a JSON file func initConfig() error { // Default configuration state.Config = Config{ ServerPort: "8080", - LogFile: "vivotek_events.log", + LogFile: "nvr_events.log", } // Try to load from config file if it exists @@ -74,9 +108,7 @@ func initConfig() error { logOutput = file } - state.Logger = log.New(logOutput, "VIVOTEK-API: ", log.LstdFlags) - - fmt.Printf("Config loaded, handing off logs to %s...\n", state.Config.LogFile) + state.Logger = log.New(logOutput, "NVR-API: ", log.LstdFlags) return nil } @@ -91,7 +123,7 @@ func basicAuth(next http.HandlerFunc) http.HandlerFunc { username, password, ok := r.BasicAuth() if !ok || username != state.Config.AuthUsername || password != state.Config.AuthPassword { - w.Header().Set("WWW-Authenticate", `Basic realm="Vivotek NVR API"`) + w.Header().Set("WWW-Authenticate", `Basic realm="NVR API"`) w.WriteHeader(http.StatusUnauthorized) w.Write([]byte("Unauthorized")) return @@ -145,22 +177,187 @@ func handleEvent(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(response) } -// processEvent handles different event types -func processEvent(event *VivotekEvent) { - switch event.EventType { - case "MotionDetection": - handleMotionEvent(event) - case "VideoLoss": - handleVideoLossEvent(event) - case "DeviceConnection": - handleConnectionEvent(event) - default: - state.Logger.Printf("Unhandled event type: %s", event.EventType) +// handleHikVisionAlarm processes alarm events from HIKVision devices +func handleHikVisionAlarm(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost && r.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + w.Write([]byte("Only POST and GET methods are supported")) + return } - // Forward to notification URL if configured - if state.Config.NotifyURL != "" { - forwardEvent(event) + // Check for specific HIK authentication if enabled + if state.Config.HikEnabled && state.Config.HikUsername != "" { + username, password, ok := r.BasicAuth() + if !ok || username != state.Config.HikUsername || password != state.Config.HikPassword { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("Unauthorized for HIKVision integration")) + return + } + } + + // Read the request body + body, err := io.ReadAll(r.Body) + if err != nil { + state.Logger.Printf("Error reading HIKVision request body: %v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + // Parse the XML alarm data + var hikAlarm HIKVisionAlarm + err = xml.Unmarshal(body, &hikAlarm) + if err != nil { + state.Logger.Printf("Error parsing HIKVision XML: %v", err) + state.Logger.Printf("Raw payload: %s", string(body)) + w.WriteHeader(http.StatusBadRequest) + return + } + + // Convert to our standard event format + event := convertHikVisionAlarm(hikAlarm, string(body)) + + // Log the event + state.EventCount++ + state.Logger.Printf("Received HIKVision alarm #%d: Type=%s, Device=%s, Channel=%s", + state.EventCount, event.EventType, event.DeviceID, event.ChannelID) + + // Process the event based on type + processEvent(&event) + + // Respond with success + w.WriteHeader(http.StatusOK) + response := map[string]interface{}{ + "status": "success", + "message": "HIKVision alarm processed successfully", + "eventId": state.EventCount, + } + + // HIKVision may expect XML response, but most implementations work fine with JSON + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +// convertHikVisionAlarm converts HIKVision alarm format to our standard event format +func convertHikVisionAlarm(hikAlarm HIKVisionAlarm, rawXML string) HikVisionEvent { + // Parse the datetime from HIKVision format + eventTime, err := time.Parse("2006-01-02T15:04:05-07:00", hikAlarm.DateTime) + if err != nil { + // If standard format fails, try alternative formats + eventTime, err = time.Parse("2006-01-02T15:04:05Z", hikAlarm.DateTime) + if err != nil { + // If all parsing fails, use current time + eventTime = time.Now() + } + } + + // Map HIKVision event types to standardized types + eventType := mapHikEventType(hikAlarm.EventType) + + // Create device ID from IP if available + deviceID := fmt.Sprintf("HIK_%s", hikAlarm.IPAddress) + if hikAlarm.MacAddress != "" { + deviceID = fmt.Sprintf("HIK_%s", strings.ReplaceAll(hikAlarm.MacAddress, ":", "")) + } + + // Create channel ID + channelID := fmt.Sprintf("Channel%d", hikAlarm.ChannelID) + + // Create event details map + eventDetails := map[string]interface{}{ + "source": "HIKVision", + "ipAddress": hikAlarm.IPAddress, + "description": hikAlarm.EventDescription, + "state": hikAlarm.EventState, + "macAddress": hikAlarm.MacAddress, + "originalType": hikAlarm.EventType, + } + + // Add optional fields if present + if hikAlarm.DetectionRegionID > 0 { + eventDetails["regionId"] = hikAlarm.DetectionRegionID + } + + return HikVisionEvent{ + EventType: eventType, + EventTime: eventTime, + DeviceID: deviceID, + ChannelID: channelID, + EventDetails: eventDetails, + RawXML: rawXML, + } +} + +// mapHikEventType converts HIKVision event types to our standardized types +func mapHikEventType(hikType string) string { + // Map HIKVision event types to standardized types + // HIKVision has many event types, this is a simplified mapping + hikType = strings.ToLower(hikType) + + switch { + case strings.Contains(hikType, "motion"): + return "MotionDetection" + case strings.Contains(hikType, "videoloss"): + return "VideoLoss" + case strings.Contains(hikType, "tamper") || strings.Contains(hikType, "shelteralarm"): + return "TamperDetection" + case strings.Contains(hikType, "disk"): + return "StorageFailure" + case strings.Contains(hikType, "line") || strings.Contains(hikType, "crossing"): + return "LineCrossing" + case strings.Contains(hikType, "intrusion"): + return "IntrusionDetection" + case strings.Contains(hikType, "face"): + return "FaceDetection" + case strings.Contains(hikType, "io") || strings.Contains(hikType, "alarm"): + return "IOAlarm" + case strings.Contains(hikType, "connection"): + return "DeviceConnection" + default: + return "UnknownEvent_" + hikType + } +} + +// processEvent handles different event types +func processEvent(event interface{}) { + // Process based on event type + switch e := event.(type) { + case *VivotekEvent: + switch e.EventType { + case "MotionDetection": + handleMotionEvent(e) + case "VideoLoss": + handleVideoLossEvent(e) + case "DeviceConnection": + handleConnectionEvent(e) + default: + state.Logger.Printf("Unhandled Vivotek event type: %s", e.EventType) + } + + // Forward to notification URL if configured + if state.Config.NotifyURL != "" { + forwardEvent(e) + } + + case *HikVisionEvent: + switch e.EventType { + case "MotionDetection": + handleHikMotionEvent(e) + case "VideoLoss": + handleHikVideoLossEvent(e) + case "LineCrossing", "IntrusionDetection": + handleHikSmartEvent(e) + case "IOAlarm": + handleHikIOAlarmEvent(e) + case "DeviceConnection": + handleHikConnectionEvent(e) + default: + state.Logger.Printf("Unhandled HIKVision event type: %s", e.EventType) + } + + // Forward to notification URL if configured + if state.Config.NotifyURL != "" { + forwardHikEvent(e) + } } // Send to Telegram if enabled @@ -187,6 +384,37 @@ func handleConnectionEvent(event *VivotekEvent) { // Add custom processing for connection events } +// handleHikMotionEvent processes HIKVision motion detection events +func handleHikMotionEvent(event *HikVisionEvent) { + state.Logger.Printf("HIKVision motion detected on device %s, channel %s", event.DeviceID, event.ChannelID) + // Add custom processing for HIKVision motion events +} + +// handleHikVideoLossEvent processes HIKVision video loss events +func handleHikVideoLossEvent(event *HikVisionEvent) { + state.Logger.Printf("HIKVision video lost on device %s, channel %s", event.DeviceID, event.ChannelID) + // Add custom processing for HIKVision video loss events +} + +// handleHikSmartEvent processes HIKVision smart events (line crossing, intrusion) +func handleHikSmartEvent(event *HikVisionEvent) { + state.Logger.Printf("HIKVision smart event %s on device %s, channel %s", + event.EventType, event.DeviceID, event.ChannelID) + // Add custom processing for HIKVision smart events +} + +// handleHikIOAlarmEvent processes HIKVision IO alarm events +func handleHikIOAlarmEvent(event *HikVisionEvent) { + state.Logger.Printf("HIKVision IO alarm on device %s, channel %s", event.DeviceID, event.ChannelID) + // Add custom processing for HIKVision IO events +} + +// handleHikConnectionEvent processes HIKVision device connection events +func handleHikConnectionEvent(event *HikVisionEvent) { + state.Logger.Printf("HIKVision connection event for device %s", event.DeviceID) + // Add custom processing for HIKVision connection events +} + // forwardEvent sends the event to a configured notification URL func forwardEvent(event *VivotekEvent) { eventJSON, err := json.Marshal(event) @@ -207,9 +435,29 @@ func forwardEvent(event *VivotekEvent) { } } +// forwardHikEvent forwards HIKVision events to notification URL +func forwardHikEvent(event *HikVisionEvent) { + eventJSON, err := json.Marshal(event) + if err != nil { + state.Logger.Printf("Error serializing HIKVision event for forwarding: %v", err) + return + } + + resp, err := http.Post(state.Config.NotifyURL, "application/json", bytes.NewBuffer(eventJSON)) + if err != nil { + state.Logger.Printf("Error forwarding HIKVision event: %v", err) + return + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + state.Logger.Printf("Error response from notification URL for HIKVision event: %d", resp.StatusCode) + } +} + // sendTelegramNotification sends event information to a Telegram chat/bot -func sendTelegramNotification(event *VivotekEvent) { - // Format the message +func sendTelegramNotification(event interface{}) { + // Format the message based on event type message := formatTelegramMessage(event) // Construct the Telegram Bot API URL @@ -234,48 +482,111 @@ func sendTelegramNotification(event *VivotekEvent) { body, _ := io.ReadAll(resp.Body) state.Logger.Printf("Telegram API error: status=%d, response=%s", resp.StatusCode, string(body)) } else { - state.Logger.Printf("Telegram notification sent successfully for event type %s", event.EventType) + // Log success based on event type + switch e := event.(type) { + case *VivotekEvent: + state.Logger.Printf("Telegram notification sent successfully for Vivotek event type %s", e.EventType) + case *HikVisionEvent: + state.Logger.Printf("Telegram notification sent successfully for HIKVision event type %s", e.EventType) + default: + state.Logger.Printf("Telegram notification sent successfully for unknown event type") + } } } // formatTelegramMessage creates a human-readable message for Telegram -func formatTelegramMessage(event *VivotekEvent) string { - // Basic message with event details - message := fmt.Sprintf("🚨 Vivotek NVR Alert\n\n"+ - "Event: %s\n"+ - "Time: %s\n"+ - "Device: %s\n"+ - "Channel: %s\n", - event.EventType, - event.EventTime.Format("2006-01-02 15:04:05"), - event.DeviceID, - event.ChannelID) +func formatTelegramMessage(event interface{}) string { + var message string - // Add custom message based on event type - switch event.EventType { - case "MotionDetection": - message += "📹 Motion detected!" + switch e := event.(type) { + case *VivotekEvent: + // Basic message with event details + message = fmt.Sprintf("🚨 NVR Alert\n\n"+ + "Event: %s\n"+ + "Time: %s\n"+ + "Device: %s\n"+ + "Channel: %s\n", + e.EventType, + e.EventTime.Format("2006-01-02 15:04:05"), + e.DeviceID, + e.ChannelID) - // Add zone info if available - if zone, ok := event.EventDetails["zoneId"].(string); ok { - message += fmt.Sprintf(" (Zone: %s)", zone) + // Add custom message based on event type + switch e.EventType { + case "MotionDetection": + message += "📹 Motion detected!" + + // Add zone info if available + if zone, ok := e.EventDetails["zoneId"].(string); ok { + message += fmt.Sprintf(" (Zone: %s)", zone) + } + + case "VideoLoss": + message += "⚠️ Video signal lost! Please check camera connection." + + case "DeviceConnection": + if status, ok := e.EventDetails["status"].(string); ok && status == "disconnected" { + message += "❌ Device disconnected! Network issue possible." + } else { + message += "✅ Device connected and operating normally." + } + + default: + // Add any available details for unknown event types + detailsJSON, _ := json.Marshal(e.EventDetails) + if len(detailsJSON) > 0 { + message += fmt.Sprintf("\n
%s
", string(detailsJSON)) + } } - case "VideoLoss": - message += "⚠️ Video signal lost! Please check camera connection." + case *HikVisionEvent: + // HIKVision specific formatting + message = fmt.Sprintf("🔔 HIKVision Alarm\n\n"+ + "Event: %s\n"+ + "Time: %s\n"+ + "Device: %s\n"+ + "Channel: %s\n", + e.EventType, + e.EventTime.Format("2006-01-02 15:04:05"), + e.DeviceID, + e.ChannelID) - case "DeviceConnection": - if status, ok := event.EventDetails["status"].(string); ok && status == "disconnected" { - message += "❌ Device disconnected! Network issue possible." - } else { - message += "✅ Device connected and operating normally." + // Add description if available + if desc, ok := e.EventDetails["description"].(string); ok && desc != "" { + message += fmt.Sprintf("Description: %s\n", desc) } - default: - // Add any available details for unknown event types - detailsJSON, _ := json.Marshal(event.EventDetails) - if len(detailsJSON) > 0 { - message += fmt.Sprintf("\n
%s
", string(detailsJSON)) + // Add custom message based on event type + switch e.EventType { + case "MotionDetection": + message += "📹 Motion detected!" + + case "LineCrossing": + message += "🚷 Line crossing detected!" + + case "IntrusionDetection": + message += "🚨 Intrusion detected!" + + case "FaceDetection": + message += "👤 Face detected!" + + case "IOAlarm": + message += "🔌 I/O Alarm triggered!" + + case "TamperDetection": + message += "⚠️ Camera tampering detected!" + + case "VideoLoss": + message += "⚠️ Video signal lost!" + + case "StorageFailure": + message += "💾 Storage failure! Check NVR hard drive." + + default: + // For unknown events, include available details + if state, ok := e.EventDetails["state"].(string); ok { + message += fmt.Sprintf("\nState: %s", state) + } } } @@ -294,12 +605,9 @@ func healthCheck(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(response) } -var startTime time.Time - func main() { startTime = time.Now() fmt.Print("Starting NVR API...\n") - // Initialize configuration if err := initConfig(); err != nil { log.Fatalf("Failed to initialize configuration: %v", err) @@ -310,6 +618,9 @@ func main() { http.HandleFunc("/event", basicAuth(handleEvent)) http.HandleFunc("/events", basicAuth(handleEvent)) // Alternative endpoint + // Add HIKVision alarm server endpoint + http.HandleFunc("/hikvision/alarm/hikvision/alarm", basicAuth(handleHikVisionAlarm)) + // Start the HTTP server serverAddr := fmt.Sprintf(":%s", state.Config.ServerPort) state.Logger.Printf("Starting NVR Event Handler API on %s", serverAddr)