You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
507 lines
11 KiB
507 lines
11 KiB
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),
|
|
}
|
|
}
|