|
|
package light
import ( "context" "database/sql" "log" "net/http" "sync" "time"
"git.aiterp.net/lucifer/lucifer/internal/httperr" "git.aiterp.net/lucifer/lucifer/models" "golang.org/x/sync/errgroup" )
// ErrUnknownDriver is returned by any function asking for a driver name if the driver specified doesn't exist.
var ErrUnknownDriver = httperr.Error{Status: http.StatusNotImplemented, Kind: "unknown_driver", Message: "Unknown light driver"}
// A Service wraps the repos for lights and bridges and takes care of the business logic.
type Service struct { mutex sync.Mutex
bridges models.BridgeRepository lights models.LightRepository buttons models.ButtonRepository groups models.GroupRepository
buttonActive map[int]bool buttonTargets map[int]int }
// DirectConnect connects to a bridge directly, without going through the discovery process to find them..
func (s *Service) DirectConnect(ctx context.Context, driver string, addr string, name string) (models.Bridge, error) { d, ok := drivers[driver] if !ok { return models.Bridge{}, ErrUnknownDriver }
bridge := models.Bridge{ ID: -1, Name: name, Addr: addr, Driver: driver, }
bridge, err := d.Connect(ctx, bridge) if err != nil { return models.Bridge{}, httperr.Error{Status: http.StatusPreconditionFailed, Kind: "connect_failed", Message: err.Error()} }
bridge, err = s.bridges.Insert(ctx, bridge) if err != nil { return models.Bridge{}, err }
return bridge, nil }
// SyncLights syncs all lights in a bridge with the state in the database.
func (s *Service) SyncLights(ctx context.Context, bridge models.Bridge) error { d, ok := drivers[bridge.Driver] if !ok { return ErrUnknownDriver }
bridgeLights, err := d.Lights(ctx, bridge) if err != nil { return err }
dbLights, err := s.lights.ListByBridge(ctx, bridge) if err != nil { return err }
// Add unknown lights
for _, bridgeLight := range bridgeLights { found := false for _, dbLight := range dbLights { if dbLight.InternalID == bridgeLight.InternalID { found = true break } }
// Add unknown lights if it doesn't exist in the databse.
if !found { log.Println("Adding unknown light", bridgeLight.InternalID) bridgeLight.SetColor("FFFFFF") bridgeLight.Brightness = 254 bridgeLight.On = true _, err := s.lights.Insert(ctx, bridgeLight) if err != nil { return err } } }
changedLights, err := d.ChangedLights(ctx, bridge, dbLights...) if err != nil { return err }
if len(changedLights) > 0 { err := d.Apply(ctx, bridge, changedLights...) if err != nil { log.Printf("Failed to apply one or more of the changes on bridge %d (%s): %s", bridge.ID, bridge.Name, err) } }
return nil }
// SyncButtons syncs all buttons in a bridge with the state in the database.
func (s *Service) SyncButtons(ctx context.Context, bridge models.Bridge) error { d, ok := drivers[bridge.Driver] if !ok { return ErrUnknownDriver }
bridgeButtons, err := d.Buttons(ctx, bridge) if err != nil { return err }
dbButtons, err := s.buttons.ListByBridge(ctx, bridge) if err != nil { return err }
changedButtons := make([]models.Button, 0, len(bridgeButtons)) newButtons := make([]models.Button, 0, len(dbButtons)) exists := make(map[string]bool, len(dbButtons))
// Check for new or changed
for _, bridgeButton := range bridgeButtons { found := false
exists[bridgeButton.InternalID] = true
for _, dbButton := range dbButtons { if bridgeButton.InternalID == dbButton.InternalID { if dbButton.InternalIndex != bridgeButton.InternalIndex || dbButton.Missing { dbButton.InternalIndex = bridgeButton.InternalIndex dbButton.Missing = false changedButtons = append(changedButtons, dbButton)
log.Println("Updating button", dbButton.ID) }
found = true
break } }
if !found { newButtons = append(newButtons, bridgeButton) } }
// Check for missing buttons
for _, dbButton := range dbButtons { if !exists[dbButton.InternalID] { dbButton.Missing = true changedButtons = append(changedButtons, dbButton)
log.Println("Marking button", dbButton.ID, "as missing") } }
// Insert new buttons
for _, newButton := range newButtons { button, err := s.buttons.Insert(ctx, newButton) if err != nil { log.Printf("Failed to insert button %s (%s): %s", button.Name, button.InternalID, err) continue }
dbButtons = append(dbButtons, button) }
// Update changed buttons
for _, changedButton := range changedButtons { err := s.buttons.Update(ctx, changedButton) if err != nil { log.Printf("Failed to change button %s (%d): %s", changedButton.Name, changedButton.ID, err) continue }
for i := range dbButtons { if dbButtons[i].ID == changedButton.ID { dbButtons[i] = changedButton
break } } }
// Start polling buttons
for _, dbButton := range dbButtons { s.mutex.Lock() if exists[dbButton.InternalID] && !s.buttonActive[dbButton.ID] { s.buttonActive[dbButton.ID] = true go s.pollButton(ctx, bridge, dbButton)
log.Printf("Polling button %s (%d)", dbButton.Name, dbButton.ID) }
s.buttonTargets[dbButton.ID] = dbButton.TargetGroupID s.mutex.Unlock() }
return nil }
// UpdateLight updates the light immediately.
func (s *Service) UpdateLight(ctx context.Context, light models.Light) error { bridge, err := s.bridges.FindByID(ctx, light.BridgeID) if err != nil { return httperr.NotFound("Bridge") } d, ok := drivers[bridge.Driver] if !ok { return ErrUnknownDriver }
err = s.lights.Update(ctx, light) if err != nil { return err }
return d.Apply(ctx, bridge, light) }
// SyncLoop runs synclight on all bridges twice every second until the context is
// done.
func (s *Service) SyncLoop(ctx context.Context) { interval := time.NewTicker(time.Millisecond * 2500)
for { select { case <-interval.C: { bridges, err := s.Bridges(context.Background()) if err != nil { log.Println("Could not get bridges:", err) }
for _, bridge := range bridges { err = s.SyncLights(ctx, bridge) if err != nil { log.Printf("Light sync failed for bridge %s (%d): %s", bridge.Name, bridge.ID, err) break }
err = s.SyncButtons(ctx, bridge) if err != nil { log.Printf("Button sync failed for bridge %s (%d): %s", bridge.Name, bridge.ID, err) break } } } case <-ctx.Done(): { log.Println("Sync loop stopped.") return } } } }
// Bridge gets a bridge by ID.
func (s *Service) Bridge(ctx context.Context, id int) (models.Bridge, error) { return s.bridges.FindByID(ctx, id) }
// Bridges gets all known bridges.
func (s *Service) Bridges(ctx context.Context) ([]models.Bridge, error) { return s.bridges.List(ctx) }
// Light gets all known bridges.
func (s *Service) Light(ctx context.Context, id int) (models.Light, error) { return s.lights.FindByID(ctx, id) }
// DeleteBridge deletes the bridge.
func (s *Service) DeleteBridge(ctx context.Context, bridge models.Bridge) error { s.mutex.Lock() defer s.mutex.Unlock()
err := s.bridges.Remove(ctx, bridge) if err != nil { return err }
lights, err := s.lights.ListByBridge(ctx, bridge) if err != nil { return err }
for _, light := range lights { err := s.lights.Remove(ctx, light) if err != nil { return err } }
return nil }
// DeleteBridgeLight deletes the light in a bridge.
func (s *Service) DeleteBridgeLight(ctx context.Context, bridge models.Bridge, light models.Light) error { s.mutex.Lock() defer s.mutex.Unlock()
d, ok := drivers[bridge.Driver] if !ok { return ErrUnknownDriver }
err := d.ForgetLight(ctx, bridge, light) if err != nil { return err }
err = s.lights.Remove(ctx, light) if err != nil { return err }
return nil }
// UpdateBridge updates a bridge.
func (s *Service) UpdateBridge(ctx context.Context, bridge models.Bridge) error { s.mutex.Lock() defer s.mutex.Unlock()
err := s.bridges.Update(ctx, bridge) if err != nil { return err }
return nil }
// DiscoverLights discovers new lights.
func (s *Service) DiscoverLights(ctx context.Context, bridge models.Bridge) error { d, ok := drivers[bridge.Driver] if !ok { return ErrUnknownDriver }
return d.DiscoverLights(ctx, bridge) }
// DiscoverBridges discovers new lights.
func (s *Service) DiscoverBridges(ctx context.Context, driver string) ([]models.Bridge, error) { d, ok := drivers[driver] if !ok { return nil, ErrUnknownDriver }
existingBridges, err := s.Bridges(ctx) if err != nil { return nil, err }
newBridges, err := d.Bridges(ctx) if err != nil { return nil, err }
for _, existingBridge := range existingBridges { for j, newBridge := range newBridges { if newBridge.InternalID == existingBridge.InternalID { newBridges = append(newBridges[:j], newBridges[j+1:]...) break } } }
return newBridges, nil }
func (s *Service) pollButton(ctx context.Context, bridge models.Bridge, button models.Button) { defer func() { s.mutex.Lock() s.buttonActive[button.ID] = false s.mutex.Unlock() }()
d, ok := drivers[bridge.Driver] if !ok { log.Printf("Could not listen on button %s (%d) because the driver %s is unknwon", button.Name, button.ID, bridge.Driver) return }
events, err := d.PollButton(ctx, bridge, button) if err != nil { log.Printf("Could not listen on button %s (%d) because the driver %s is unknwon", button.Name, button.ID, bridge.Driver) return }
for event := range events { s.mutex.Lock() targetGroupID := s.buttonTargets[button.ID] s.mutex.Unlock()
if targetGroupID < 0 { continue }
if event.Kind != models.ButtonEventKindPress && event.Kind != models.ButtonEventKindRepeat { continue }
group, err := s.groups.FindByID(ctx, targetGroupID) if err != nil { if err != sql.ErrNoRows { log.Println("Group not found:", err, targetGroupID) }
continue }
lights, err := s.lights.ListByGroup(ctx, group)
log.Printf("Got button input for group %s (%d) (id: %d, idx: %d, kind: %s)", group.Name, group.ID, button.ID, event.Index, event.Kind)
eg, _ := errgroup.WithContext(ctx)
for i := range lights { light := &lights[i]
switch event.Index { case 1: { if !light.On { light.On = true
eg.Go(func() error { return s.UpdateLight(ctx, *light) }) } } case 2: { if light.Brightness < 254 { if light.Brightness >= (254 - 64) { light.Brightness = 254 } else { light.Brightness += 64 }
eg.Go(func() error { return s.UpdateLight(ctx, *light) }) } } case 3: { if light.Brightness > 0 { if light.Brightness < 64 { light.Brightness = 0 } else { light.Brightness -= 64 }
eg.Go(func() error { return s.UpdateLight(ctx, *light) }) } } case 4: { if light.On { light.On = false eg.Go(func() error { return s.UpdateLight(ctx, *light) }) } } } }
err = eg.Wait() if err != nil { log.Println("Failed to update one or more lights:", err) } } }
// NewService creates a new light.Service.
func NewService(bridges models.BridgeRepository, lights models.LightRepository, groups models.GroupRepository, buttons models.ButtonRepository) *Service { return &Service{ bridges: bridges, lights: lights, buttons: buttons, groups: groups,
buttonActive: make(map[int]bool, 64), buttonTargets: make(map[int]int, 64), } }
|