mirror of
https://github.com/Warky-Devs/nvr-notify-api.git
synced 2025-05-18 23:27:32 +00:00
Test case
This commit is contained in:
parent
af3a290119
commit
d973a1dcc8
1
.gitignore
vendored
1
.gitignore
vendored
@ -23,3 +23,4 @@ go.work.sum
|
|||||||
|
|
||||||
# env file
|
# env file
|
||||||
.env
|
.env
|
||||||
|
vivotek_events.log
|
||||||
|
38
Dockerfile
Normal file
38
Dockerfile
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
FROM golang:1.22-alpine AS builder
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy go.mod and go.sum files
|
||||||
|
COPY go.mod ./
|
||||||
|
COPY go.sum ./
|
||||||
|
|
||||||
|
# Download dependencies
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
#COPY *.go ./
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY ./cmd ./cmd
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -o nvr-api cmd/apisrv/main.go
|
||||||
|
|
||||||
|
# Create a minimal production image
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy the executable from the builder stage
|
||||||
|
COPY --from=builder /app/nvr-api .
|
||||||
|
|
||||||
|
# Copy config file
|
||||||
|
COPY config.sample.json /app/config.json
|
||||||
|
|
||||||
|
# Create directory for logs
|
||||||
|
RUN mkdir -p /app/logs
|
||||||
|
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
CMD ["./nvr-api"]
|
120
README.md
120
README.md
@ -1,2 +1,120 @@
|
|||||||
# nvr-notify-api
|
# nvr-notify-api
|
||||||
A Notify API rest service that can be used with an NVR
|
|
||||||
|
A Go-based API server that receives and processes HTTP event notifications from Vivotek Network Video Recorders (NVRs).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Receives event notifications from Vivotek NVR devices
|
||||||
|
- Processes different event types (motion detection, video loss, device connection)
|
||||||
|
- Configurable logging
|
||||||
|
- Optional HTTP Basic Authentication
|
||||||
|
- Event forwarding to external notification services
|
||||||
|
- Telegram integration for instant notifications
|
||||||
|
- Health check endpoint
|
||||||
|
- Docker support
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The application can be configured using the `config.json` file:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"server_port": "8080",
|
||||||
|
"log_file": "vivotek_events.log",
|
||||||
|
"notify_url": "https://your-notification-service.com/webhook",
|
||||||
|
"auth_username": "admin",
|
||||||
|
"auth_password": "your-secure-password",
|
||||||
|
"telegram_enabled": true,
|
||||||
|
"telegram_token": "YOUR_TELEGRAM_BOT_TOKEN",
|
||||||
|
"telegram_chat_id": "YOUR_CHAT_ID"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuration options:
|
||||||
|
- `server_port`: Port the HTTP server will listen on
|
||||||
|
- `log_file`: Path to log file (use "stdout" to log to console)
|
||||||
|
- `notify_url`: Optional URL to forward events to
|
||||||
|
- `auth_username` and `auth_password`: Optional Basic Authentication credentials
|
||||||
|
- `telegram_enabled`: Set to true to enable Telegram notifications
|
||||||
|
- `telegram_token`: Your Telegram bot token (obtained from @BotFather)
|
||||||
|
- `telegram_chat_id`: Your Telegram chat ID where notifications should be sent
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
- `/event` or `/events`: POST endpoint for receiving event notifications
|
||||||
|
- `/health`: GET endpoint to check service status
|
||||||
|
|
||||||
|
## Event Format
|
||||||
|
|
||||||
|
The API expects events in JSON format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"eventType": "MotionDetection",
|
||||||
|
"eventTime": "2023-06-15T14:30:00Z",
|
||||||
|
"deviceId": "NVR123456",
|
||||||
|
"channelId": "Camera01",
|
||||||
|
"eventDetails": {
|
||||||
|
"zoneId": "Zone1",
|
||||||
|
"confidence": 85
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the Application
|
||||||
|
|
||||||
|
### Directly
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go mod tidy
|
||||||
|
go build
|
||||||
|
./vivotek-nvr-api
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the Docker image
|
||||||
|
docker build -t vivotek-nvr-api .
|
||||||
|
|
||||||
|
# Run the container
|
||||||
|
docker run -p 8080:8080 -v ./config.json:/app/config.json -v ./logs:/app/logs vivotek-nvr-api
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuring Vivotek NVR
|
||||||
|
|
||||||
|
To configure your Vivotek NVR to send events to this API:
|
||||||
|
|
||||||
|
1. Access your NVR's web interface
|
||||||
|
2. Navigate to Configuration > Event > HTTP Notification
|
||||||
|
3. Enable HTTP notifications
|
||||||
|
4. Set the URL to `http://your-server-ip:8080/event`
|
||||||
|
5. Set the authentication method if you've configured it in the API
|
||||||
|
6. Select the events you want to be notified about
|
||||||
|
7. Save the configuration
|
||||||
|
|
||||||
|
## Extending the API
|
||||||
|
|
||||||
|
To handle additional event types, modify the `processEvent` function in the main Go file and add appropriate handler functions.
|
||||||
|
|
||||||
|
## Setting Up Telegram Notifications
|
||||||
|
|
||||||
|
1. Create a Telegram bot:
|
||||||
|
- Start a chat with [@BotFather](https://t.me/botfather) on Telegram
|
||||||
|
- Send the command `/newbot` and follow the instructions
|
||||||
|
- Once created, BotFather will provide a token - copy this to your config file
|
||||||
|
|
||||||
|
2. Get your chat ID:
|
||||||
|
- Option 1: Start a chat with your bot and send a message to it
|
||||||
|
- Option 2: Send a message to [@userinfobot](https://t.me/userinfobot) to get your chat ID
|
||||||
|
- Option 3: For group chats, add [@RawDataBot](https://t.me/RawDataBot) to your group briefly
|
||||||
|
|
||||||
|
3. Update your config file:
|
||||||
|
- Set `telegram_enabled` to `true`
|
||||||
|
- Add your bot token to `telegram_token`
|
||||||
|
- Add your chat ID to `telegram_chat_id`
|
||||||
|
|
||||||
|
4. Test the configuration:
|
||||||
|
- Start the API server
|
||||||
|
- Trigger an event from your Vivotek NVR
|
||||||
|
- You should receive a formatted message in your Telegram chat
|
316
cmd/apisrv/main.go
Normal file
316
cmd/apisrv/main.go
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Configuration for the application
|
||||||
|
type Config struct {
|
||||||
|
ServerPort string `json:"server_port"`
|
||||||
|
LogFile string `json:"log_file"`
|
||||||
|
NotifyURL string `json:"notify_url"`
|
||||||
|
AuthUsername string `json:"auth_username"`
|
||||||
|
AuthPassword string `json:"auth_password"`
|
||||||
|
TelegramEnabled bool `json:"telegram_enabled"`
|
||||||
|
TelegramToken string `json:"telegram_token"`
|
||||||
|
TelegramChatID string `json:"telegram_chat_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VivotekEvent represents the event data structure from Vivotek NVR
|
||||||
|
type VivotekEvent struct {
|
||||||
|
EventType string `json:"eventType"`
|
||||||
|
EventTime time.Time `json:"eventTime"`
|
||||||
|
DeviceID string `json:"deviceId"`
|
||||||
|
ChannelID string `json:"channelId"`
|
||||||
|
EventDetails map[string]interface{} `json:"eventDetails"`
|
||||||
|
// Add more fields as needed based on Vivotek's event structure
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalState maintains the application state
|
||||||
|
type GlobalState struct {
|
||||||
|
Config Config
|
||||||
|
EventCount int
|
||||||
|
Logger *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
var state GlobalState
|
||||||
|
|
||||||
|
// initConfig loads configuration from a JSON file
|
||||||
|
func initConfig() error {
|
||||||
|
// Default configuration
|
||||||
|
state.Config = Config{
|
||||||
|
ServerPort: "8080",
|
||||||
|
LogFile: "vivotek_events.log",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to load from config file if it exists
|
||||||
|
configFile, err := os.Open("config.json")
|
||||||
|
if err == nil {
|
||||||
|
defer configFile.Close()
|
||||||
|
decoder := json.NewDecoder(configFile)
|
||||||
|
err = decoder.Decode(&state.Config)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing config file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize logger
|
||||||
|
var logOutput io.Writer
|
||||||
|
if state.Config.LogFile == "stdout" {
|
||||||
|
logOutput = os.Stdout
|
||||||
|
} else {
|
||||||
|
file, err := os.OpenFile(state.Config.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open log file: %v", err)
|
||||||
|
}
|
||||||
|
logOutput = file
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Logger = log.New(logOutput, "VIVOTEK-API: ", log.LstdFlags)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// basicAuth implements HTTP Basic Authentication middleware
|
||||||
|
func basicAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Skip auth if credentials are not configured
|
||||||
|
if state.Config.AuthUsername == "" || state.Config.AuthPassword == "" {
|
||||||
|
next(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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.WriteHeader(http.StatusUnauthorized)
|
||||||
|
w.Write([]byte("Unauthorized"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleEvent processes events from Vivotek NVR
|
||||||
|
func handleEvent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
w.Write([]byte("Only POST method is supported"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the request body
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
state.Logger.Printf("Error reading request body: %v", err)
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the event
|
||||||
|
var event VivotekEvent
|
||||||
|
if err := json.Unmarshal(body, &event); err != nil {
|
||||||
|
state.Logger.Printf("Error parsing event JSON: %v", err)
|
||||||
|
state.Logger.Printf("Raw payload: %s", string(body))
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the event
|
||||||
|
state.EventCount++
|
||||||
|
state.Logger.Printf("Received event #%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": "Event processed successfully",
|
||||||
|
"eventId": state.EventCount,
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward to notification URL if configured
|
||||||
|
if state.Config.NotifyURL != "" {
|
||||||
|
forwardEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send to Telegram if enabled
|
||||||
|
if state.Config.TelegramEnabled && state.Config.TelegramToken != "" && state.Config.TelegramChatID != "" {
|
||||||
|
sendTelegramNotification(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMotionEvent processes motion detection events
|
||||||
|
func handleMotionEvent(event *VivotekEvent) {
|
||||||
|
state.Logger.Printf("Motion detected on device %s, channel %s", event.DeviceID, event.ChannelID)
|
||||||
|
// Add custom processing for motion events
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleVideoLossEvent processes video loss events
|
||||||
|
func handleVideoLossEvent(event *VivotekEvent) {
|
||||||
|
state.Logger.Printf("Video lost on device %s, channel %s", event.DeviceID, event.ChannelID)
|
||||||
|
// Add custom processing for video loss events
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleConnectionEvent processes device connection/disconnection events
|
||||||
|
func handleConnectionEvent(event *VivotekEvent) {
|
||||||
|
state.Logger.Printf("Connection event for device %s", event.DeviceID)
|
||||||
|
// Add custom processing for connection events
|
||||||
|
}
|
||||||
|
|
||||||
|
// forwardEvent sends the event to a configured notification URL
|
||||||
|
func forwardEvent(event *VivotekEvent) {
|
||||||
|
eventJSON, err := json.Marshal(event)
|
||||||
|
if err != nil {
|
||||||
|
state.Logger.Printf("Error serializing 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 event: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
state.Logger.Printf("Error response from notification URL: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendTelegramNotification sends event information to a Telegram chat/bot
|
||||||
|
func sendTelegramNotification(event *VivotekEvent) {
|
||||||
|
// Format the message
|
||||||
|
message := formatTelegramMessage(event)
|
||||||
|
|
||||||
|
// Construct the Telegram Bot API URL
|
||||||
|
apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", state.Config.TelegramToken)
|
||||||
|
|
||||||
|
// Prepare the request data
|
||||||
|
data := url.Values{}
|
||||||
|
data.Set("chat_id", state.Config.TelegramChatID)
|
||||||
|
data.Set("text", message)
|
||||||
|
data.Set("parse_mode", "HTML") // Enable HTML formatting
|
||||||
|
|
||||||
|
// Send the request
|
||||||
|
resp, err := http.PostForm(apiURL, data)
|
||||||
|
if err != nil {
|
||||||
|
state.Logger.Printf("Error sending Telegram notification: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Check for error response
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatTelegramMessage creates a human-readable message for Telegram
|
||||||
|
func formatTelegramMessage(event *VivotekEvent) string {
|
||||||
|
// Basic message with event details
|
||||||
|
message := fmt.Sprintf("<b>🚨 Vivotek NVR Alert</b>\n\n"+
|
||||||
|
"<b>Event:</b> %s\n"+
|
||||||
|
"<b>Time:</b> %s\n"+
|
||||||
|
"<b>Device:</b> %s\n"+
|
||||||
|
"<b>Channel:</b> %s\n",
|
||||||
|
event.EventType,
|
||||||
|
event.EventTime.Format("2006-01-02 15:04:05"),
|
||||||
|
event.DeviceID,
|
||||||
|
event.ChannelID)
|
||||||
|
|
||||||
|
// Add custom message based on event type
|
||||||
|
switch event.EventType {
|
||||||
|
case "MotionDetection":
|
||||||
|
message += "📹 <b>Motion detected!</b>"
|
||||||
|
|
||||||
|
// Add zone info if available
|
||||||
|
if zone, ok := event.EventDetails["zoneId"].(string); ok {
|
||||||
|
message += fmt.Sprintf(" (Zone: %s)", zone)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "VideoLoss":
|
||||||
|
message += "⚠️ <b>Video signal lost!</b> Please check camera connection."
|
||||||
|
|
||||||
|
case "DeviceConnection":
|
||||||
|
if status, ok := event.EventDetails["status"].(string); ok && status == "disconnected" {
|
||||||
|
message += "❌ <b>Device disconnected!</b> Network issue possible."
|
||||||
|
} else {
|
||||||
|
message += "✅ <b>Device connected</b> and operating normally."
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Add any available details for unknown event types
|
||||||
|
detailsJSON, _ := json.Marshal(event.EventDetails)
|
||||||
|
if len(detailsJSON) > 0 {
|
||||||
|
message += fmt.Sprintf("\n<pre>%s</pre>", string(detailsJSON))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
// healthCheck provides a simple endpoint to verify the service is running
|
||||||
|
func healthCheck(w http.ResponseWriter, r *http.Request) {
|
||||||
|
response := map[string]interface{}{
|
||||||
|
"status": "ok",
|
||||||
|
"eventCount": state.EventCount,
|
||||||
|
"uptime": time.Since(startTime).String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
var startTime time.Time
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
startTime = time.Now()
|
||||||
|
|
||||||
|
// Initialize configuration
|
||||||
|
if err := initConfig(); err != nil {
|
||||||
|
log.Fatalf("Failed to initialize configuration: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up HTTP routes
|
||||||
|
http.HandleFunc("/health", healthCheck)
|
||||||
|
http.HandleFunc("/event", basicAuth(handleEvent))
|
||||||
|
http.HandleFunc("/events", basicAuth(handleEvent)) // Alternative endpoint
|
||||||
|
|
||||||
|
// Start the HTTP server
|
||||||
|
serverAddr := fmt.Sprintf(":%s", state.Config.ServerPort)
|
||||||
|
state.Logger.Printf("Starting Vivotek NVR Event Handler API on %s", serverAddr)
|
||||||
|
if err := http.ListenAndServe(serverAddr, nil); err != nil {
|
||||||
|
state.Logger.Fatalf("Failed to start server: %v", err)
|
||||||
|
}
|
||||||
|
}
|
232
cmd/client/batch/main.go
Normal file
232
cmd/client/batch/main.go
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Command line flags
|
||||||
|
var (
|
||||||
|
serverURL string
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
concurrency int
|
||||||
|
scenarioFile string
|
||||||
|
outputResults bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestScenario represents a collection of test events to send
|
||||||
|
type TestScenario struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Events []EventConfig `json:"events"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventConfig represents a single event configuration
|
||||||
|
type EventConfig struct {
|
||||||
|
EventType string `json:"eventType"`
|
||||||
|
DeviceID string `json:"deviceId"`
|
||||||
|
ChannelID string `json:"channelId"`
|
||||||
|
DelaySeconds int `json:"delaySeconds"`
|
||||||
|
EventDetails map[string]interface{} `json:"eventDetails"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VivotekEvent matches the structure expected by the API
|
||||||
|
type VivotekEvent struct {
|
||||||
|
EventType string `json:"eventType"`
|
||||||
|
EventTime time.Time `json:"eventTime"`
|
||||||
|
DeviceID string `json:"deviceId"`
|
||||||
|
ChannelID string `json:"channelId"`
|
||||||
|
EventDetails map[string]interface{} `json:"eventDetails"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result tracks the outcome of sending an event
|
||||||
|
type Result struct {
|
||||||
|
Event EventConfig `json:"event"`
|
||||||
|
StatusCode int `json:"statusCode"`
|
||||||
|
Response string `json:"response"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
Duration int64 `json:"durationMs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Define command line flags
|
||||||
|
flag.StringVar(&serverURL, "url", "http://localhost:8080/event", "API server URL")
|
||||||
|
flag.StringVar(&username, "user", "", "Basic auth username")
|
||||||
|
flag.StringVar(&password, "pass", "", "Basic auth password")
|
||||||
|
flag.IntVar(&concurrency, "concurrency", 1, "Number of concurrent requests")
|
||||||
|
flag.StringVar(&scenarioFile, "scenario", "test_scenario.json", "JSON file with test scenarios")
|
||||||
|
flag.BoolVar(&outputResults, "output", false, "Output results to results.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Load test scenario
|
||||||
|
scenario, err := loadScenario(scenarioFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load scenario: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Running scenario: %s\n", scenario.Name)
|
||||||
|
fmt.Printf("Description: %s\n", scenario.Description)
|
||||||
|
fmt.Printf("Events: %d\n", len(scenario.Events))
|
||||||
|
fmt.Printf("Concurrency: %d\n", concurrency)
|
||||||
|
fmt.Println("=======================")
|
||||||
|
|
||||||
|
// Create a channel to hold the work and results
|
||||||
|
jobs := make(chan EventConfig, len(scenario.Events))
|
||||||
|
results := make(chan Result, len(scenario.Events))
|
||||||
|
|
||||||
|
// Start worker pool
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for w := 1; w <= concurrency; w++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go worker(w, jobs, results, &wg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add jobs to the queue
|
||||||
|
for _, event := range scenario.Events {
|
||||||
|
jobs <- event
|
||||||
|
}
|
||||||
|
close(jobs) // Close the jobs channel when all jobs are added
|
||||||
|
|
||||||
|
// Wait for all workers to finish in a separate goroutine
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(results) // Close results when all workers are done
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Collect results
|
||||||
|
var allResults []Result
|
||||||
|
for result := range results {
|
||||||
|
allResults = append(allResults, result)
|
||||||
|
if result.Error != "" {
|
||||||
|
fmt.Printf("❌ Error sending %s event: %s\n", result.Event.EventType, result.Error)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("✅ Sent %s event to %s: Status %d (%dms)\n",
|
||||||
|
result.Event.EventType,
|
||||||
|
result.Event.DeviceID,
|
||||||
|
result.StatusCode,
|
||||||
|
result.Duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output results if requested
|
||||||
|
if outputResults && len(allResults) > 0 {
|
||||||
|
resultsJSON, err := json.MarshalIndent(allResults, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to marshal results: %v", err)
|
||||||
|
} else {
|
||||||
|
err = os.WriteFile("results.json", resultsJSON, 0644)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to write results file: %v", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("Results written to results.json")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("=======================")
|
||||||
|
fmt.Printf("Test scenario completed: %d events sent\n", len(allResults))
|
||||||
|
}
|
||||||
|
|
||||||
|
// worker processes jobs from the jobs channel
|
||||||
|
func worker(id int, jobs <-chan EventConfig, results chan<- Result, wg *sync.WaitGroup) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
for j := range jobs {
|
||||||
|
// Apply configured delay
|
||||||
|
if j.DelaySeconds > 0 {
|
||||||
|
time.Sleep(time.Duration(j.DelaySeconds) * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the event
|
||||||
|
result := sendEvent(j)
|
||||||
|
results <- result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendEvent sends a single event to the API
|
||||||
|
func sendEvent(config EventConfig) Result {
|
||||||
|
startTime := time.Now()
|
||||||
|
result := Result{
|
||||||
|
Event: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the event
|
||||||
|
event := VivotekEvent{
|
||||||
|
EventType: config.EventType,
|
||||||
|
EventTime: time.Now(),
|
||||||
|
DeviceID: config.DeviceID,
|
||||||
|
ChannelID: config.ChannelID,
|
||||||
|
EventDetails: config.EventDetails,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal to JSON
|
||||||
|
payload, err := json.Marshal(event)
|
||||||
|
if err != nil {
|
||||||
|
result.Error = fmt.Sprintf("error creating JSON payload: %v", err)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the request
|
||||||
|
req, err := http.NewRequest("POST", serverURL, bytes.NewBuffer(payload))
|
||||||
|
if err != nil {
|
||||||
|
result.Error = fmt.Sprintf("error creating request: %v", err)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set headers
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// Add basic auth if credentials were provided
|
||||||
|
if username != "" {
|
||||||
|
req.SetBasicAuth(username, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the request
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
result.Error = fmt.Sprintf("error sending request: %v", err)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Record status code
|
||||||
|
result.StatusCode = resp.StatusCode
|
||||||
|
result.Duration = time.Since(startTime).Milliseconds()
|
||||||
|
|
||||||
|
// Record error for non-2xx responses
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
result.Error = fmt.Sprintf("server returned status code %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadScenario loads a test scenario from a JSON file
|
||||||
|
func loadScenario(filename string) (*TestScenario, error) {
|
||||||
|
file, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading scenario file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var scenario TestScenario
|
||||||
|
err = json.Unmarshal(file, &scenario)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing scenario file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &scenario, nil
|
||||||
|
}
|
88
cmd/client/batch/test_scenarion.json
Normal file
88
cmd/client/batch/test_scenarion.json
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
{
|
||||||
|
"name": "Comprehensive Vivotek NVR Event Test",
|
||||||
|
"description": "Tests various event types and scenarios for the Vivotek NVR API",
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventType": "MotionDetection",
|
||||||
|
"deviceId": "NVR001",
|
||||||
|
"channelId": "Camera01",
|
||||||
|
"delaySeconds": 0,
|
||||||
|
"eventDetails": {
|
||||||
|
"zoneId": "MainEntrance",
|
||||||
|
"confidence": 95
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventType": "MotionDetection",
|
||||||
|
"deviceId": "NVR001",
|
||||||
|
"channelId": "Camera02",
|
||||||
|
"delaySeconds": 1,
|
||||||
|
"eventDetails": {
|
||||||
|
"zoneId": "BackYard",
|
||||||
|
"confidence": 75
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventType": "VideoLoss",
|
||||||
|
"deviceId": "NVR001",
|
||||||
|
"channelId": "Camera03",
|
||||||
|
"delaySeconds": 2,
|
||||||
|
"eventDetails": {
|
||||||
|
"duration": 15,
|
||||||
|
"cause": "cable disconnected"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventType": "DeviceConnection",
|
||||||
|
"deviceId": "NVR002",
|
||||||
|
"channelId": "",
|
||||||
|
"delaySeconds": 3,
|
||||||
|
"eventDetails": {
|
||||||
|
"status": "disconnected",
|
||||||
|
"reason": "network failure"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventType": "DeviceConnection",
|
||||||
|
"deviceId": "NVR002",
|
||||||
|
"channelId": "",
|
||||||
|
"delaySeconds": 5,
|
||||||
|
"eventDetails": {
|
||||||
|
"status": "connected",
|
||||||
|
"reason": "network restored"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventType": "MotionDetection",
|
||||||
|
"deviceId": "NVR001",
|
||||||
|
"channelId": "Camera01",
|
||||||
|
"delaySeconds": 2,
|
||||||
|
"eventDetails": {
|
||||||
|
"zoneId": "MainEntrance",
|
||||||
|
"confidence": 98,
|
||||||
|
"objectType": "person"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventType": "TamperDetection",
|
||||||
|
"deviceId": "NVR001",
|
||||||
|
"channelId": "Camera04",
|
||||||
|
"delaySeconds": 3,
|
||||||
|
"eventDetails": {
|
||||||
|
"type": "covered",
|
||||||
|
"severity": "high"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eventType": "StorageFailure",
|
||||||
|
"deviceId": "NVR001",
|
||||||
|
"channelId": "",
|
||||||
|
"delaySeconds": 2,
|
||||||
|
"eventDetails": {
|
||||||
|
"disk": "HDD1",
|
||||||
|
"errorCode": "S-404",
|
||||||
|
"remainingSpace": "50MB"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
133
cmd/client/test.readme.md
Normal file
133
cmd/client/test.readme.md
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
# Vivotek NVR API Test Client
|
||||||
|
|
||||||
|
This repository contains two test clients for the Vivotek NVR Event Handler API:
|
||||||
|
|
||||||
|
1. **Single Event Test Client** (`vivotek-test-client.go`) - For sending individual test events with customizable parameters
|
||||||
|
2. **Batch Test Client** (`vivotek-test-batch.go`) - For running complex test scenarios defined in JSON
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Go 1.18 or higher
|
||||||
|
- Network access to your Vivotek NVR API server
|
||||||
|
|
||||||
|
## Single Event Test Client
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -o vivotek-test vivotek-test-client.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./vivotek-test [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Flag | Description | Default |
|
||||||
|
|------|-------------|---------|
|
||||||
|
| `-url` | API server URL | `http://localhost:8080/event` |
|
||||||
|
| `-type` | Event type | `MotionDetection` |
|
||||||
|
| `-device` | Device ID | `NVR12345` |
|
||||||
|
| `-channel` | Channel ID | `Camera01` |
|
||||||
|
| `-zone` | Detection zone (for motion events) | `Zone1` |
|
||||||
|
| `-user` | Basic auth username | `""` |
|
||||||
|
| `-pass` | Basic auth password | `""` |
|
||||||
|
| `-insecure` | Skip TLS verification | `false` |
|
||||||
|
| `-repeat` | Number of events to send | `1` |
|
||||||
|
| `-interval` | Interval between events in seconds | `5` |
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
Test a motion detection event:
|
||||||
|
```bash
|
||||||
|
./vivotek-test -type=MotionDetection -device=NVR001 -channel=Camera01 -zone=FrontDoor
|
||||||
|
```
|
||||||
|
|
||||||
|
Test video loss with authentication:
|
||||||
|
```bash
|
||||||
|
./vivotek-test -type=VideoLoss -device=NVR001 -channel=Camera02 -user=admin -pass=password
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate multiple events:
|
||||||
|
```bash
|
||||||
|
./vivotek-test -type=DeviceConnection -device=NVR002 -repeat=5 -interval=10
|
||||||
|
```
|
||||||
|
|
||||||
|
## Batch Test Client
|
||||||
|
|
||||||
|
The batch test client allows you to define complex test scenarios in a JSON file and execute them with a single command.
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -o vivotek-batch vivotek-test-batch.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./vivotek-batch [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Flag | Description | Default |
|
||||||
|
|------|-------------|---------|
|
||||||
|
| `-url` | API server URL | `http://localhost:8080/event` |
|
||||||
|
| `-user` | Basic auth username | `""` |
|
||||||
|
| `-pass` | Basic auth password | `""` |
|
||||||
|
| `-concurrency` | Number of concurrent requests | `1` |
|
||||||
|
| `-scenario` | JSON file with test scenarios | `test_scenario.json` |
|
||||||
|
| `-output` | Output results to results.json | `false` |
|
||||||
|
|
||||||
|
### Test Scenario Format
|
||||||
|
|
||||||
|
The test scenario file uses the following format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Test Scenario Name",
|
||||||
|
"description": "Description of the test scenario",
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventType": "MotionDetection",
|
||||||
|
"deviceId": "NVR001",
|
||||||
|
"channelId": "Camera01",
|
||||||
|
"delaySeconds": 0,
|
||||||
|
"eventDetails": {
|
||||||
|
"zoneId": "Zone1",
|
||||||
|
"confidence": 95
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
Run the default test scenario:
|
||||||
|
```bash
|
||||||
|
./vivotek-batch
|
||||||
|
```
|
||||||
|
|
||||||
|
Run a custom scenario with authentication:
|
||||||
|
```bash
|
||||||
|
./vivotek-batch -scenario=my_scenario.json -user=admin -pass=password
|
||||||
|
```
|
||||||
|
|
||||||
|
Run with multiple concurrent requests:
|
||||||
|
```bash
|
||||||
|
./vivotek-batch -concurrency=5 -output
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tips for Testing
|
||||||
|
|
||||||
|
1. **Start with Single Events**: Use the single event client first to verify basic connectivity
|
||||||
|
2. **Check Server Logs**: Monitor the server logs while running tests to see how events are being processed
|
||||||
|
3. **Verify Telegram**: If you've configured Telegram notifications, check that they're being sent
|
||||||
|
4. **Increase Load Gradually**: When testing performance, start with low concurrency and gradually increase
|
||||||
|
5. **Custom Scenarios**: Create different scenarios for different testing purposes (basic functionality, stress testing, etc.)
|
190
cmd/client/test/main.go
Normal file
190
cmd/client/test/main.go
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Command line flags
|
||||||
|
var (
|
||||||
|
serverURL string
|
||||||
|
eventType string
|
||||||
|
deviceID string
|
||||||
|
channelID string
|
||||||
|
zone string
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
insecure bool
|
||||||
|
repeatCount int
|
||||||
|
interval int
|
||||||
|
)
|
||||||
|
|
||||||
|
// VivotekEvent matches the structure expected by the API
|
||||||
|
type VivotekEvent struct {
|
||||||
|
EventType string `json:"eventType"`
|
||||||
|
EventTime time.Time `json:"eventTime"`
|
||||||
|
DeviceID string `json:"deviceId"`
|
||||||
|
ChannelID string `json:"channelId"`
|
||||||
|
EventDetails map[string]interface{} `json:"eventDetails"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Define command line flags
|
||||||
|
flag.StringVar(&serverURL, "url", "http://localhost:8080/event", "API server URL")
|
||||||
|
flag.StringVar(&eventType, "type", "MotionDetection", "Event type (MotionDetection, VideoLoss, DeviceConnection)")
|
||||||
|
flag.StringVar(&deviceID, "device", "NVR12345", "Device ID")
|
||||||
|
flag.StringVar(&channelID, "channel", "Camera01", "Channel ID")
|
||||||
|
flag.StringVar(&zone, "zone", "Zone1", "Detection zone (for motion events)")
|
||||||
|
flag.StringVar(&username, "user", "", "Basic auth username")
|
||||||
|
flag.StringVar(&password, "pass", "", "Basic auth password")
|
||||||
|
flag.BoolVar(&insecure, "insecure", false, "Skip TLS verification")
|
||||||
|
flag.IntVar(&repeatCount, "repeat", 1, "Number of events to send")
|
||||||
|
flag.IntVar(&interval, "interval", 5, "Interval between events in seconds")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Print client configuration
|
||||||
|
fmt.Println("Vivotek API Test Client")
|
||||||
|
fmt.Println("=======================")
|
||||||
|
fmt.Printf("Server URL: %s\n", serverURL)
|
||||||
|
fmt.Printf("Event Type: %s\n", eventType)
|
||||||
|
fmt.Printf("Device ID: %s\n", deviceID)
|
||||||
|
fmt.Printf("Channel ID: %s\n", channelID)
|
||||||
|
if eventType == "MotionDetection" {
|
||||||
|
fmt.Printf("Zone: %s\n", zone)
|
||||||
|
}
|
||||||
|
fmt.Printf("Auth: %v\n", username != "")
|
||||||
|
fmt.Printf("Sending %d events with %d second intervals\n", repeatCount, interval)
|
||||||
|
fmt.Println("=======================")
|
||||||
|
|
||||||
|
// Configure HTTP client with optional TLS settings
|
||||||
|
httpClient := &http.Client{}
|
||||||
|
if insecure {
|
||||||
|
httpClient.Transport = &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send events
|
||||||
|
for i := 1; i <= repeatCount; i++ {
|
||||||
|
if i > 1 {
|
||||||
|
fmt.Printf("Waiting %d seconds...\n", interval)
|
||||||
|
time.Sleep(time.Duration(interval) * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Sending event %d of %d\n", i, repeatCount)
|
||||||
|
err := sendEvent(httpClient)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to send event: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("All events sent successfully!")
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendEvent(client *http.Client) error {
|
||||||
|
// Create event details based on event type
|
||||||
|
eventDetails := make(map[string]interface{})
|
||||||
|
|
||||||
|
switch eventType {
|
||||||
|
case "MotionDetection":
|
||||||
|
eventDetails["zoneId"] = zone
|
||||||
|
eventDetails["confidence"] = 85
|
||||||
|
case "VideoLoss":
|
||||||
|
eventDetails["duration"] = 30
|
||||||
|
eventDetails["cause"] = "cable disconnected"
|
||||||
|
case "DeviceConnection":
|
||||||
|
eventDetails["status"] = "disconnected"
|
||||||
|
eventDetails["reason"] = "network failure"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the event payload
|
||||||
|
event := VivotekEvent{
|
||||||
|
EventType: eventType,
|
||||||
|
EventTime: time.Now(),
|
||||||
|
DeviceID: deviceID,
|
||||||
|
ChannelID: channelID,
|
||||||
|
EventDetails: eventDetails,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal to JSON
|
||||||
|
payload, err := json.MarshalIndent(event, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating JSON payload: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the payload for debugging
|
||||||
|
fmt.Println("Event payload:")
|
||||||
|
fmt.Println(string(payload))
|
||||||
|
|
||||||
|
// Create the request
|
||||||
|
req, err := http.NewRequest("POST", serverURL, bytes.NewBuffer(payload))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set headers
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// Add basic auth if credentials were provided
|
||||||
|
if username != "" {
|
||||||
|
req.SetBasicAuth(username, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the request
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error sending request: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Read the response
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the status code
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return fmt.Errorf("server returned error: %d - %s", resp.StatusCode, string(respBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the response
|
||||||
|
fmt.Printf("Response status: %d\n", resp.StatusCode)
|
||||||
|
fmt.Printf("Response: %s\n", string(respBody))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventGenerator returns a function that creates custom events
|
||||||
|
func EventGenerator() func(string, string, string) VivotekEvent {
|
||||||
|
return func(eventType, deviceID, channelID string) VivotekEvent {
|
||||||
|
eventDetails := make(map[string]interface{})
|
||||||
|
switch eventType {
|
||||||
|
case "MotionDetection":
|
||||||
|
eventDetails["zoneId"] = "Zone1"
|
||||||
|
eventDetails["confidence"] = 85
|
||||||
|
case "VideoLoss":
|
||||||
|
eventDetails["duration"] = 30
|
||||||
|
case "DeviceConnection":
|
||||||
|
eventDetails["status"] = "connected"
|
||||||
|
}
|
||||||
|
|
||||||
|
return VivotekEvent{
|
||||||
|
EventType: eventType,
|
||||||
|
EventTime: time.Now(),
|
||||||
|
DeviceID: deviceID,
|
||||||
|
ChannelID: channelID,
|
||||||
|
EventDetails: eventDetails,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
config.sample.json
Normal file
11
config.sample.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"server_port": "8080",
|
||||||
|
"log_file": "vivotek_events.log",
|
||||||
|
"notify_url": "https://your-notification-service.com/webhook",
|
||||||
|
"auth_username": "admin",
|
||||||
|
"auth_password": "your-secure-password",
|
||||||
|
"telegram_enabled": true,
|
||||||
|
"telegram_token": "YOUR_TELEGRAM_BOT_TOKEN",
|
||||||
|
"telegram_chat_id": "YOUR_CHAT_ID"
|
||||||
|
}
|
||||||
|
|
12
docker_build_and_install.sh
Executable file
12
docker_build_and_install.sh
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
|
||||||
|
docker build . --build-arg CACHEBUST=$(date +%s) -t localdev/nvr-api
|
||||||
|
echo Installing....
|
||||||
|
docker stop nvr-api
|
||||||
|
docker rm nvr-api
|
||||||
|
#docker volume create --name nvr-api
|
||||||
|
|
||||||
|
#docker run -d -p 8082:8080 -v /tmp/config.json:/app/config.json --name nvr-api --restart unless-stopped --memory=2G --cpus=1 localdev/nvr-api
|
||||||
|
docker run -d -p 8082:8080 --name nvr-api --restart unless-stopped --memory=2G --cpus=1 localdev/nvr-api
|
||||||
|
|
54
telegram-integration.svg
Normal file
54
telegram-integration.svg
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 400">
|
||||||
|
<!-- Background -->
|
||||||
|
<rect width="800" height="400" fill="#f5f5f5"/>
|
||||||
|
|
||||||
|
<!-- Vivotek NVR -->
|
||||||
|
<rect x="50" y="150" width="120" height="100" rx="10" fill="#0078d7" stroke="#005a9e" stroke-width="2"/>
|
||||||
|
<text x="110" y="200" font-family="Arial" font-size="16" fill="white" text-anchor="middle">Vivotek NVR</text>
|
||||||
|
|
||||||
|
<!-- API Server -->
|
||||||
|
<rect x="340" y="150" width="120" height="100" rx="10" fill="#4caf50" stroke="#388e3c" stroke-width="2"/>
|
||||||
|
<text x="400" y="200" font-family="Arial" font-size="16" fill="white" text-anchor="middle">Go API Server</text>
|
||||||
|
|
||||||
|
<!-- Telegram -->
|
||||||
|
<rect x="630" y="150" width="120" height="100" rx="10" fill="#2196f3" stroke="#1976d2" stroke-width="2"/>
|
||||||
|
<text x="690" y="182" font-family="Arial" font-size="16" fill="white" text-anchor="middle">Telegram</text>
|
||||||
|
<text x="690" y="210" font-family="Arial" font-size="14" fill="white" text-anchor="middle">Bot API</text>
|
||||||
|
|
||||||
|
<!-- Camera Icon -->
|
||||||
|
<circle cx="80" y="120" r="15" fill="#0078d7" stroke="#005a9e" stroke-width="1"/>
|
||||||
|
<rect x="70" y="105" width="20" height="10" rx="2" fill="#0078d7" stroke="#005a9e" stroke-width="1"/>
|
||||||
|
|
||||||
|
<!-- Mobile Phone -->
|
||||||
|
<rect x="680" y="80" width="30" height="50" rx="3" fill="#2196f3" stroke="#1976d2" stroke-width="1"/>
|
||||||
|
<rect x="685" y="85" width="20" height="30" rx="1" fill="white" stroke="#1976d2" stroke-width="0.5"/>
|
||||||
|
<circle cx="695" y="123" r="3" fill="white" stroke="#1976d2" stroke-width="0.5"/>
|
||||||
|
|
||||||
|
<!-- HTTP Request Arrow -->
|
||||||
|
<line x1="170" y1="200" x2="320" y2="200" stroke="#333" stroke-width="2" marker-end="url(#arrow)"/>
|
||||||
|
<text x="245" y="190" font-family="Arial" font-size="12" fill="#333" text-anchor="middle">HTTP Event</text>
|
||||||
|
|
||||||
|
<!-- Telegram API Arrow -->
|
||||||
|
<line x1="460" y1="200" x2="610" y2="200" stroke="#333" stroke-width="2" marker-end="url(#arrow)"/>
|
||||||
|
<text x="535" y="190" font-family="Arial" font-size="12" fill="#333" text-anchor="middle">Notification</text>
|
||||||
|
|
||||||
|
<!-- Motion detected messages -->
|
||||||
|
<rect x="500" y="100" width="120" height="60" rx="15" fill="white" stroke="#2196f3" stroke-width="1.5"/>
|
||||||
|
<text x="560" y="125" font-family="Arial" font-size="12" fill="#333" text-anchor="middle">🚨 Motion</text>
|
||||||
|
<text x="560" y="145" font-family="Arial" font-size="12" fill="#333" text-anchor="middle">Detected!</text>
|
||||||
|
|
||||||
|
<!-- Arrow marker definition -->
|
||||||
|
<defs>
|
||||||
|
<marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
|
||||||
|
<path d="M0,0 L0,6 L9,3 z" fill="#333"/>
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<text x="400" y="40" font-family="Arial" font-size="24" font-weight="bold" fill="#333" text-anchor="middle">Vivotek NVR Telegram Integration</text>
|
||||||
|
|
||||||
|
<!-- Labels -->
|
||||||
|
<text x="110" y="270" font-family="Arial" font-size="14" fill="#333" text-anchor="middle">Event Source</text>
|
||||||
|
<text x="400" y="270" font-family="Arial" font-size="14" fill="#333" text-anchor="middle">Event Processor</text>
|
||||||
|
<text x="690" y="270" font-family="Arial" font-size="14" fill="#333" text-anchor="middle">Notification</text>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.0 KiB |
Loading…
Reference in New Issue
Block a user