package internal import ( "github.com/godbus/dbus/v5" "log" "sort" "strings" "time" ) type Monitor struct { scanner *Scanner keyboard *KeyboardInfo callback func(*KeyboardInfo) ticker *time.Ticker stopCh chan struct{} notifications *NotificationManager config *Config lastConnected bool } func NewMonitor(scanner *Scanner, config *Config) (*Monitor, error) { notifications, err := NewNotificationManager() if err != nil { log.Printf("Warning: Could not initialize notifications: %v", err) notifications = nil } return &Monitor{ scanner: scanner, stopCh: make(chan struct{}), keyboard: &KeyboardInfo{LeftBattery: -1, RightBattery: -1}, notifications: notifications, config: config, lastConnected: false, }, nil } func (m *Monitor) SetDevice(name, address string) { m.keyboard.Name = name m.keyboard.Address = address } func (m *Monitor) SetCallback(cb func(*KeyboardInfo)) { m.callback = cb } func (m *Monitor) UpdateConfig(config *Config) { m.config = config } func (m *Monitor) Start(interval time.Duration) { m.ticker = time.NewTicker(interval) go m.run() } func (m *Monitor) Stop() { if m.ticker != nil { m.ticker.Stop() } if m.notifications != nil { m.notifications.Close() } close(m.stopCh) } func (m *Monitor) UpdateInterval(interval time.Duration) { if m.ticker != nil { m.ticker.Reset(interval) } } func (m *Monitor) run() { m.update() // Initial update for { select { case <-m.ticker.C: m.update() case <-m.stopCh: return } } } func (m *Monitor) update() { _, path, err := m.scanner.FindDevice(m.keyboard.Address) if err != nil || path == "" { m.keyboard.Connected = false m.handleConnectionChange() m.notify() return } m.keyboard.Connected = m.scanner.IsConnected(path) m.handleConnectionChange() if !m.keyboard.Connected { m.notify() return } prevLeft, prevRight := m.keyboard.LeftBattery, m.keyboard.RightBattery m.getBatteryLevels(path) if m.config.ShowNotifications && m.notifications != nil { m.checkLowBattery(prevLeft, prevRight) } m.notify() } func (m *Monitor) handleConnectionChange() { if m.config.ShowNotifications && m.notifications != nil && m.lastConnected != m.keyboard.Connected { m.notifications.ShowConnectionNotification(m.keyboard.Name, m.keyboard.Connected) } m.lastConnected = m.keyboard.Connected } func (m *Monitor) checkLowBattery(prevLeft, prevRight int) { threshold := m.config.LowBatteryThreshold // Only notify when battery drops to/below threshold, not on every update if m.keyboard.LeftBattery >= 0 && m.keyboard.LeftBattery <= threshold { if prevLeft < 0 || prevLeft > threshold { m.notifications.ShowLowBatteryNotification("Left", m.keyboard.LeftBattery, threshold) } } if m.keyboard.RightBattery >= 0 && m.keyboard.RightBattery <= threshold { if prevRight < 0 || prevRight > threshold { m.notifications.ShowLowBatteryNotification("Right", m.keyboard.RightBattery, threshold) } } } func (m *Monitor) getBatteryLevels(devicePath dbus.ObjectPath) { m.keyboard.LeftBattery, m.keyboard.RightBattery = -1, -1 objects, err := m.scanner.GetManagedObjects() if err != nil { return } var services []dbus.ObjectPath prefix := string(devicePath) + "/" for path, ifaces := range objects { if !strings.HasPrefix(string(path), prefix) { continue } if serviceProps, ok := ifaces["org.bluez.GattService1"]; ok { if uuid, ok := serviceProps["UUID"].Value().(string); ok && strings.ToLower(uuid) == "0000180f-0000-1000-8000-00805f9b34fb" { services = append(services, path) } } } sort.Slice(services, func(i, j int) bool { return services[i] < services[j] }) for i, service := range services { if level := m.readBatteryLevel(service, objects); level >= 0 { if i == 0 { m.keyboard.LeftBattery = level } else if i == 1 { m.keyboard.RightBattery = level } } } // Fallback to legacy method if len(services) == 1 && m.keyboard.RightBattery == -1 { if level, err := m.scanner.GetLegacyBattery(devicePath); err == nil { m.keyboard.LeftBattery = level } } if m.keyboard.LeftBattery >= 0 || m.keyboard.RightBattery >= 0 { m.keyboard.LastUpdate = time.Now() } } func (m *Monitor) readBatteryLevel(servicePath dbus.ObjectPath, objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant) int { prefix := string(servicePath) + "/" for path, ifaces := range objects { if !strings.HasPrefix(string(path), prefix) { continue } if charProps, ok := ifaces["org.bluez.GattCharacteristic1"]; ok { if uuid, ok := charProps["UUID"].Value().(string); ok && strings.ToLower(uuid) == "00002a19-0000-1000-8000-00805f9b34fb" { if data, err := m.scanner.ReadCharacteristic(path); err == nil && len(data) > 0 { level := int(data[0]) if level == 255 { return -1 } return level } } } } return -1 } func (m *Monitor) notify() { if m.callback != nil { m.callback(m.keyboard) } } func (m *Monitor) GetKeyboard() *KeyboardInfo { return m.keyboard }