package light import ( "context" "log" "net/http" "sync" "time" "git.aiterp.net/lucifer/lucifer/internal/httperr" "git.aiterp.net/lucifer/lucifer/models" ) // 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 } // 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 { s.mutex.Lock() defer s.mutex.Unlock() 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 _, 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 } // UpdateLight updates the light immediately. func (s *Service) UpdateLight(ctx context.Context, light models.Light) error { s.mutex.Lock() defer s.mutex.Unlock() err := s.lights.Update(ctx, light) if err != nil { return err } bridge, err := s.bridges.FindByID(ctx, light.BridgeID) if err != nil { return httperr.NotFound("Bridge") } d, ok := drivers[bridge.Driver] if !ok { return ErrUnknownDriver } 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.Println("Sync failed:", err) } } } 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) } // 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 } // 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) } // NewService creates a new light.Service. func NewService(bridges models.BridgeRepository, lights models.LightRepository) *Service { return &Service{ bridges: bridges, lights: lights, } }