210 lines
4.9 KiB
Go
210 lines
4.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"math"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/time/rate"
|
|
)
|
|
|
|
type bmDeviceResponse struct {
|
|
ID int `json:"id"`
|
|
LastSeen string `json:"last_seen"`
|
|
Tx string `json:"tx"`
|
|
Rx string `json:"rx"`
|
|
Status int `json:"status"`
|
|
StatusText string `json:"statusText"`
|
|
Hardware string `json:"hardware"`
|
|
Firmware string `json:"firmware"`
|
|
Pep int `json:"pep"`
|
|
Agl int `json:"agl"`
|
|
Website string `json:"website"`
|
|
PriorityDescription string `json:"priorityDescription"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
func startBMDeviceSync(db *sql.DB) {
|
|
go func() {
|
|
runBMDeviceSync(db)
|
|
|
|
ticker := time.NewTicker(3 * 24 * time.Hour)
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
runBMDeviceSync(db)
|
|
}
|
|
}()
|
|
}
|
|
|
|
func runBMDeviceSync(db *sql.DB) {
|
|
log.Println("Starting BM device sync...")
|
|
|
|
type repeaterFreq struct {
|
|
ID int
|
|
Callsign string
|
|
FreqTx float64
|
|
FreqRx float64
|
|
}
|
|
|
|
// Only poll repeaters that haven't been polled in the last 3 days
|
|
rows, err := db.Query(`SELECT id, callsign, freq_tx, freq_rx FROM repeaters
|
|
WHERE network = 'Brandmeister' AND hotspot = 0
|
|
AND (last_polled IS NULL OR last_polled < NOW() - INTERVAL '3 days')`)
|
|
if err != nil {
|
|
log.Printf("BM device sync: failed to query repeaters: %v", err)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var rptrs []repeaterFreq
|
|
for rows.Next() {
|
|
var rf repeaterFreq
|
|
if err := rows.Scan(&rf.ID, &rf.Callsign, &rf.FreqTx, &rf.FreqRx); err != nil {
|
|
continue
|
|
}
|
|
rptrs = append(rptrs, rf)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
log.Printf("BM device sync: row iteration error: %v", err)
|
|
return
|
|
}
|
|
|
|
log.Printf("BM device sync: checking %d BrandMeister repeaters", len(rptrs))
|
|
|
|
// Rate limit: 3600 requests/hour = 1/second
|
|
limiter := rate.NewLimiter(rate.Every(time.Second), 1)
|
|
sem := make(chan struct{}, 5) // max 5 concurrent
|
|
var wg sync.WaitGroup
|
|
|
|
client := &http.Client{Timeout: 15 * time.Second}
|
|
ctx := context.Background()
|
|
|
|
var mu sync.Mutex
|
|
updated, errors, inactive := 0, 0, 0
|
|
inactiveThreshold := time.Now().Add(-7 * 24 * time.Hour)
|
|
|
|
for _, rf := range rptrs {
|
|
wg.Add(1)
|
|
go func(rf repeaterFreq) {
|
|
defer wg.Done()
|
|
|
|
sem <- struct{}{}
|
|
defer func() { <-sem }()
|
|
|
|
if err := limiter.Wait(ctx); err != nil {
|
|
return
|
|
}
|
|
|
|
url := fmt.Sprintf("https://api.brandmeister.network/v2/device/%d", rf.ID)
|
|
resp, err := client.Get(url)
|
|
if err != nil {
|
|
mu.Lock()
|
|
errors++
|
|
mu.Unlock()
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
mu.Lock()
|
|
errors++
|
|
mu.Unlock()
|
|
return
|
|
}
|
|
|
|
var device bmDeviceResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&device); err != nil {
|
|
mu.Lock()
|
|
errors++
|
|
mu.Unlock()
|
|
return
|
|
}
|
|
|
|
lastSeen, err := time.Parse("2006-01-02 15:04:05", device.LastSeen)
|
|
if err != nil {
|
|
mu.Lock()
|
|
errors++
|
|
mu.Unlock()
|
|
return
|
|
}
|
|
|
|
if lastSeen.Before(inactiveThreshold) {
|
|
log.Printf("BM device sync: inactive repeater %d (%s), last seen %s", rf.ID, rf.Callsign, lastSeen.Format("2006-01-02"))
|
|
mu.Lock()
|
|
inactive++
|
|
mu.Unlock()
|
|
}
|
|
|
|
// Combine priority description and description
|
|
desc := device.Description
|
|
if device.PriorityDescription != "" {
|
|
if desc != "" {
|
|
desc = device.PriorityDescription + "\n" + desc
|
|
} else {
|
|
desc = device.PriorityDescription
|
|
}
|
|
}
|
|
// Strip HTML tags for clean storage
|
|
desc = stripHTML(desc)
|
|
|
|
// Check frequency consistency between RadioID and BrandMeister
|
|
freqInconsistent := false
|
|
if bmTx, err := strconv.ParseFloat(device.Tx, 64); err == nil {
|
|
if bmRx, err := strconv.ParseFloat(device.Rx, 64); err == nil {
|
|
if math.Abs(bmTx-rf.FreqTx) > 0.001 || math.Abs(bmRx-rf.FreqRx) > 0.001 {
|
|
freqInconsistent = true
|
|
}
|
|
}
|
|
}
|
|
|
|
_, err = db.Exec(
|
|
`UPDATE repeaters SET last_seen=$1, bm_status=$2, bm_status_text=$3,
|
|
hardware=$4, firmware=$5, pep=$6, agl=$7, website=$8, description=$9,
|
|
import_freq_inconsistent=$10, last_polled=NOW()
|
|
WHERE id=$11`,
|
|
lastSeen, device.Status, device.StatusText,
|
|
device.Hardware, device.Firmware, device.Pep, device.Agl,
|
|
device.Website, desc, freqInconsistent, rf.ID,
|
|
)
|
|
|
|
mu.Lock()
|
|
if err != nil {
|
|
errors++
|
|
} else {
|
|
updated++
|
|
}
|
|
mu.Unlock()
|
|
}(rf)
|
|
}
|
|
|
|
wg.Wait()
|
|
log.Printf("BM device sync complete: %d updated, %d inactive, %d errors", updated, inactive, errors)
|
|
}
|
|
|
|
// stripHTML removes HTML tags from a string.
|
|
func stripHTML(s string) string {
|
|
var b strings.Builder
|
|
inTag := false
|
|
for _, r := range s {
|
|
if r == '<' {
|
|
inTag = true
|
|
continue
|
|
}
|
|
if r == '>' {
|
|
inTag = false
|
|
continue
|
|
}
|
|
if !inTag {
|
|
b.WriteRune(r)
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|