Files
ShackMaster/internal/api/device_manager.go
2026-01-10 16:04:38 +01:00

380 lines
11 KiB
Go

package api
import (
"log"
"sync"
"time"
"git.rouggy.com/rouggy/ShackMaster/internal/config"
"git.rouggy.com/rouggy/ShackMaster/internal/devices/antennagenius"
"git.rouggy.com/rouggy/ShackMaster/internal/devices/powergenius"
"git.rouggy.com/rouggy/ShackMaster/internal/devices/rotatorgenius"
"git.rouggy.com/rouggy/ShackMaster/internal/devices/tunergenius"
"git.rouggy.com/rouggy/ShackMaster/internal/devices/ultrabeam"
"git.rouggy.com/rouggy/ShackMaster/internal/devices/webswitch"
"git.rouggy.com/rouggy/ShackMaster/internal/services/solar"
"git.rouggy.com/rouggy/ShackMaster/internal/services/weather"
)
type DeviceManager struct {
config *config.Config
webSwitch *webswitch.Client
powerGenius *powergenius.Client
tunerGenius *tunergenius.Client
antennaGenius *antennagenius.Client
rotatorGenius *rotatorgenius.Client
ultrabeam *ultrabeam.Client
solarClient *solar.Client
weatherClient *weather.Client
hub *Hub
statusMu sync.RWMutex
lastStatus *SystemStatus
updateInterval time.Duration
stopChan chan struct{}
// Auto frequency tracking
freqThreshold int // Threshold for triggering update (Hz)
autoTrackEnabled bool
ultrabeamDirection int // User-selected direction (0=normal, 1=180, 2=bi-dir)
lastFreqUpdateTime time.Time // Last time we sent frequency update
freqUpdateCooldown time.Duration // Minimum time between updates
}
type SystemStatus struct {
WebSwitch *webswitch.Status `json:"webswitch"`
PowerGenius *powergenius.Status `json:"power_genius"`
TunerGenius *tunergenius.Status `json:"tuner_genius"`
AntennaGenius *antennagenius.Status `json:"antenna_genius"`
RotatorGenius *rotatorgenius.Status `json:"rotator_genius"`
Ultrabeam *ultrabeam.Status `json:"ultrabeam"`
Solar *solar.SolarData `json:"solar"`
Weather *weather.WeatherData `json:"weather"`
Timestamp time.Time `json:"timestamp"`
}
func NewDeviceManager(cfg *config.Config, hub *Hub) *DeviceManager {
return &DeviceManager{
config: cfg,
hub: hub,
updateInterval: 1 * time.Second, // Update status every second
stopChan: make(chan struct{}),
freqThreshold: 25000, // 25 kHz default
autoTrackEnabled: true, // Enabled by default
ultrabeamDirection: 0, // Normal direction by default
freqUpdateCooldown: 2 * time.Second, // Wait 2 seconds between updates
}
}
func (dm *DeviceManager) Initialize() error {
log.Println("Initializing device manager...")
// Initialize WebSwitch
dm.webSwitch = webswitch.New(dm.config.Devices.WebSwitch.Host)
// Initialize Power Genius
dm.powerGenius = powergenius.New(
dm.config.Devices.PowerGenius.Host,
dm.config.Devices.PowerGenius.Port,
)
// Initialize Tuner Genius
dm.tunerGenius = tunergenius.New(
dm.config.Devices.TunerGenius.Host,
dm.config.Devices.TunerGenius.Port,
)
// Initialize Antenna Genius
dm.antennaGenius = antennagenius.New(
dm.config.Devices.AntennaGenius.Host,
dm.config.Devices.AntennaGenius.Port,
)
// Initialize Rotator Genius
log.Printf("Initializing RotatorGenius: host=%s port=%d", dm.config.Devices.RotatorGenius.Host, dm.config.Devices.RotatorGenius.Port)
dm.rotatorGenius = rotatorgenius.New(
dm.config.Devices.RotatorGenius.Host,
dm.config.Devices.RotatorGenius.Port,
)
// Initialize Ultrabeam
log.Printf("Initializing Ultrabeam: host=%s port=%d", dm.config.Devices.Ultrabeam.Host, dm.config.Devices.Ultrabeam.Port)
dm.ultrabeam = ultrabeam.New(
dm.config.Devices.Ultrabeam.Host,
dm.config.Devices.Ultrabeam.Port,
)
// Initialize Solar data client
dm.solarClient = solar.New()
// Initialize Weather client
dm.weatherClient = weather.New(
dm.config.Weather.OpenWeatherMapAPIKey,
dm.config.Location.Latitude,
dm.config.Location.Longitude,
)
// Start device polling in background (non-blocking)
go func() {
if err := dm.powerGenius.Start(); err != nil {
log.Printf("Warning: Failed to start PowerGenius polling: %v", err)
}
}()
go func() {
if err := dm.tunerGenius.Start(); err != nil {
log.Printf("Warning: Failed to start TunerGenius polling: %v", err)
}
}()
go func() {
if err := dm.antennaGenius.Start(); err != nil {
log.Printf("Warning: Failed to start AntennaGenius polling: %v", err)
}
}()
log.Println("About to launch RotatorGenius goroutine...")
go func() {
log.Println("Starting RotatorGenius polling goroutine...")
if err := dm.rotatorGenius.Start(); err != nil {
log.Printf("Warning: Failed to start RotatorGenius polling: %v", err)
}
}()
log.Println("RotatorGenius goroutine launched")
log.Println("About to launch Ultrabeam goroutine...")
go func() {
log.Println("Starting Ultrabeam polling goroutine...")
if err := dm.ultrabeam.Start(); err != nil {
log.Printf("Warning: Failed to start Ultrabeam polling: %v", err)
}
}()
log.Println("Ultrabeam goroutine launched")
log.Println("Device manager initialized")
return nil
}
func (dm *DeviceManager) Start() error {
log.Println("Starting device monitoring...")
go dm.monitorDevices()
return nil
}
func (dm *DeviceManager) Stop() {
log.Println("Stopping device manager...")
close(dm.stopChan)
// Close all connections
if dm.powerGenius != nil {
dm.powerGenius.Close()
}
if dm.tunerGenius != nil {
dm.tunerGenius.Close()
}
if dm.antennaGenius != nil {
dm.antennaGenius.Close()
}
if dm.rotatorGenius != nil {
dm.rotatorGenius.Close()
}
if dm.ultrabeam != nil {
dm.ultrabeam.Stop()
}
}
func (dm *DeviceManager) monitorDevices() {
ticker := time.NewTicker(dm.updateInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
dm.updateStatus()
case <-dm.stopChan:
return
}
}
}
func (dm *DeviceManager) updateStatus() {
status := &SystemStatus{
Timestamp: time.Now(),
}
// Query all devices
// WebSwitch - get actual relay states
if wsStatus, err := dm.webSwitch.GetStatus(); err == nil {
status.WebSwitch = wsStatus
} else {
log.Printf("WebSwitch error: %v", err)
}
// Power Genius
if pgStatus, err := dm.powerGenius.GetStatus(); err == nil {
status.PowerGenius = pgStatus
} else {
log.Printf("Power Genius error: %v", err)
}
// Tuner Genius
if tgStatus, err := dm.tunerGenius.GetStatus(); err == nil {
status.TunerGenius = tgStatus
} else {
log.Printf("Tuner Genius error: %v", err)
}
// Antenna Genius
if agStatus, err := dm.antennaGenius.GetStatus(); err == nil {
status.AntennaGenius = agStatus
} else {
log.Printf("Antenna Genius error: %v", err)
}
// Rotator Genius
if rgStatus, err := dm.rotatorGenius.GetStatus(); err == nil {
status.RotatorGenius = rgStatus
} else {
log.Printf("Rotator Genius error: %v", err)
}
// Ultrabeam
if ubStatus, err := dm.ultrabeam.GetStatus(); err == nil {
status.Ultrabeam = ubStatus
// Sync direction with Ultrabeam if not yet set (first time or after restart)
// This prevents auto-track from using wrong direction before user changes it
if dm.ultrabeamDirection == 0 && ubStatus.Direction != 0 {
dm.ultrabeamDirection = ubStatus.Direction
log.Printf("Auto-track: Initialized direction from Ultrabeam: %d", dm.ultrabeamDirection)
}
} else {
log.Printf("Ultrabeam error: %v", err)
}
// Auto frequency tracking: Update Ultrabeam when TunerGenius frequency differs from Ultrabeam
if dm.autoTrackEnabled && status.TunerGenius != nil && status.TunerGenius.Connected && status.Ultrabeam != nil && status.Ultrabeam.Connected {
tunerFreqKhz := int(status.TunerGenius.FreqA) // TunerGenius frequency is already in kHz
ultrabeamFreqKhz := status.Ultrabeam.Frequency // Ultrabeam frequency in kHz
// Ignore invalid frequencies or out of Ultrabeam range (40M-6M)
// This prevents retraction when slice is closed (FreqA becomes 0)
// Ultrabeam VL2.3 only covers 7000-54000 kHz (40M to 6M)
if tunerFreqKhz < 7000 || tunerFreqKhz > 54000 {
return // Out of range, skip auto-track
}
freqDiff := tunerFreqKhz - ultrabeamFreqKhz
if freqDiff < 0 {
freqDiff = -freqDiff
}
// Convert diff to Hz for comparison with threshold (which is in Hz)
freqDiffHz := freqDiff * 1000
// Don't send command if motors are already moving
if status.Ultrabeam.MotorsMoving != 0 {
// Motors moving - wait for them to finish
return
}
if freqDiffHz >= dm.freqThreshold {
// Use current Ultrabeam direction if user hasn't explicitly set one
directionToUse := dm.ultrabeamDirection
if directionToUse == 0 && status.Ultrabeam.Direction != 0 {
directionToUse = status.Ultrabeam.Direction
}
// Check cooldown to prevent rapid fire commands
timeSinceLastUpdate := time.Since(dm.lastFreqUpdateTime)
if timeSinceLastUpdate < dm.freqUpdateCooldown {
log.Printf("Auto-track: Cooldown active (%v remaining), skipping update", dm.freqUpdateCooldown-timeSinceLastUpdate)
} else {
log.Printf("Auto-track: Frequency differs by %d kHz, updating Ultrabeam to %d kHz (direction=%d)", freqDiff, tunerFreqKhz, directionToUse)
// Send to Ultrabeam with saved or current direction
if err := dm.ultrabeam.SetFrequency(tunerFreqKhz, directionToUse); err != nil {
log.Printf("Auto-track: Failed to update Ultrabeam: %v (will retry)", err)
} else {
log.Printf("Auto-track: Successfully sent frequency to Ultrabeam")
dm.lastFreqUpdateTime = time.Now() // Update cooldown timer
}
}
}
}
// Solar Data (fetched every 15 minutes, cached)
if solarData, err := dm.solarClient.GetSolarData(); err == nil {
status.Solar = solarData
} else {
log.Printf("Solar data error: %v", err)
}
// Weather Data (fetched every 10 minutes, cached)
if weatherData, err := dm.weatherClient.GetWeatherData(); err == nil {
status.Weather = weatherData
} else {
log.Printf("Weather data error: %v", err)
}
// Update cached status
dm.statusMu.Lock()
dm.lastStatus = status
dm.statusMu.Unlock()
// Broadcast to all connected clients
if dm.hub != nil {
dm.hub.BroadcastStatusUpdate(status)
}
}
func (dm *DeviceManager) GetStatus() *SystemStatus {
dm.statusMu.RLock()
defer dm.statusMu.RUnlock()
if dm.lastStatus == nil {
return &SystemStatus{
Timestamp: time.Now(),
}
}
return dm.lastStatus
}
// Device control methods
func (dm *DeviceManager) WebSwitch() *webswitch.Client {
return dm.webSwitch
}
func (dm *DeviceManager) PowerGenius() *powergenius.Client {
return dm.powerGenius
}
func (dm *DeviceManager) TunerGenius() *tunergenius.Client {
return dm.tunerGenius
}
func (dm *DeviceManager) AntennaGenius() *antennagenius.Client {
return dm.antennaGenius
}
func (dm *DeviceManager) RotatorGenius() *rotatorgenius.Client {
return dm.rotatorGenius
}
func (dm *DeviceManager) Ultrabeam() *ultrabeam.Client {
return dm.ultrabeam
}
func (dm *DeviceManager) SetAutoTrack(enabled bool, thresholdHz int) {
dm.autoTrackEnabled = enabled
dm.freqThreshold = thresholdHz
}
func (dm *DeviceManager) SetUltrabeamDirection(direction int) {
dm.ultrabeamDirection = direction
log.Printf("Ultrabeam direction set to: %d", direction)
}