diff --git a/app/api/devices.go b/app/api/devices.go index 038dee0..9662934 100644 --- a/app/api/devices.go +++ b/app/api/devices.go @@ -7,6 +7,7 @@ import ( "git.aiterp.net/lucifer/new-server/models" "github.com/gin-gonic/gin" "log" + "strconv" "time" ) @@ -265,6 +266,46 @@ func Devices(r gin.IRoutes) { return withSceneState(devices), nil })) + + r.DELETE("/:fetch", handler(func(c *gin.Context) (interface{}, error) { + id, err := strconv.Atoi(c.Param("fetch")) + if err != nil { + return nil, err + } + + device, err := config.DeviceRepository().Find(ctxOf(c), id) + if err != nil { + return nil, err + } + + if c.Query("forgetmenot") != "true" { + bridge, err := config.BridgeRepository().Find(ctxOf(c), device.BridgeID) + if err != nil { + return nil, err + } + + driver, err := config.DriverProvider().Provide(bridge.Driver) + if err != nil { + return nil, err + } + + forgettableDriver, ok := driver.(models.ForgettableDriver) + if !ok { + return nil, models.ErrCannotForget + } + err = forgettableDriver.ForgetDevice(ctxOf(c), bridge, *device) + if err != nil { + return nil, err + } + } + + err = config.DeviceRepository().Delete(ctxOf(c), device) + if err != nil { + return nil, err + } + + return device, nil + })) } func withSceneState(devices []models.Device) []models.Device { diff --git a/internal/drivers/hue/bridge.go b/internal/drivers/hue/bridge.go index ee12768..0385e0a 100644 --- a/internal/drivers/hue/bridge.go +++ b/internal/drivers/hue/bridge.go @@ -23,6 +23,7 @@ type Bridge struct { externalID int lightStates []*hueLightState sensorStates []*hueSensorState + quarantine map[string]time.Time syncingPublish uint32 } @@ -34,6 +35,10 @@ func (b *Bridge) Refresh(ctx context.Context) error { b.mu.Lock() for index, light := range lightMap { + if time.Now().Before(b.quarantine[light.Uniqueid]) { + continue + } + var state *hueLightState for _, existingState := range b.lightStates { if existingState.index == index { @@ -68,6 +73,10 @@ func (b *Bridge) Refresh(ctx context.Context) error { b.mu.Lock() for index, sensor := range sensorMap { + if time.Now().Before(b.quarantine[sensor.UniqueID]) { + continue + } + var state *hueSensorState for _, existingState := range b.sensorStates { if existingState.index == index { @@ -137,6 +146,44 @@ func (b *Bridge) SyncStale(ctx context.Context) error { return eg.Wait() } +func (b *Bridge) ForgetDevice(ctx context.Context, device models.Device) error { + // Find index + b.mu.Lock() + found := false + index := -1 + for i, ls := range b.lightStates { + if ls.uniqueID == device.InternalID { + found = true + index = i + } + } + b.mu.Unlock() + if !found { + return models.ErrNotFound + } + + // Delete light from bridge + err := b.deleteLight(ctx, index) + if err != nil { + return err + } + + // Remove light state from local list. I don't know if the quarantine is necessary, but let's have it anyway. + b.mu.Lock() + for i, ls := range b.lightStates { + if ls.uniqueID == device.InternalID { + b.lightStates = append(b.lightStates[:i], b.lightStates[i+1:]...) + } + } + if b.quarantine == nil { + b.quarantine = make(map[string]time.Time, 1) + } + b.quarantine[device.InternalID] = time.Now().Add(time.Second * 30) + b.mu.Unlock() + + return nil +} + func (b *Bridge) SyncSensors(ctx context.Context) ([]models.Event, error) { sensorMap, err := b.getSensors(ctx) if err != nil { @@ -175,6 +222,10 @@ func (b *Bridge) putGroupLightState(ctx context.Context, index int, input LightS return b.put(ctx, fmt.Sprintf("groups/%d/action", index), input, nil) } +func (b *Bridge) deleteLight(ctx context.Context, index int) error { + return b.delete(ctx, fmt.Sprintf("lights/%d", index), nil) +} + func (b *Bridge) getToken(ctx context.Context) (string, error) { result := make([]CreateUserResponse, 0, 1) err := b.post(ctx, "", CreateUserInput{DeviceType: "git.aiterp.net/lucifer"}, &result) diff --git a/internal/drivers/hue/driver.go b/internal/drivers/hue/driver.go index 86d579a..9da87cf 100644 --- a/internal/drivers/hue/driver.go +++ b/internal/drivers/hue/driver.go @@ -372,6 +372,15 @@ func (d *Driver) Run(ctx context.Context, bridge models.Bridge, ch chan<- models } } +func (d *Driver) ForgetDevice(ctx context.Context, bridge models.Bridge, device models.Device) error { + b, err := d.ensureBridge(ctx, bridge) + if err != nil { + return err + } + + return b.ForgetDevice(ctx, device) +} + func (d *Driver) ensureBridge(ctx context.Context, info models.Bridge) (*Bridge, error) { d.mu.Lock() for _, bridge := range d.bridges { diff --git a/models/driver.go b/models/driver.go index 291962f..2dd37ea 100644 --- a/models/driver.go +++ b/models/driver.go @@ -16,3 +16,7 @@ type Driver interface { Publish(ctx context.Context, bridge Bridge, devices []Device) error Run(ctx context.Context, bridge Bridge, ch chan<- Event) error } + +type ForgettableDriver interface { + ForgetDevice(ctx context.Context, bridge Bridge, device Device) error +} diff --git a/models/errors.go b/models/errors.go index cff288d..ff337ba 100644 --- a/models/errors.go +++ b/models/errors.go @@ -16,6 +16,7 @@ var ErrUnexpectedResponse = errors.New("driver api returned unexpected response var ErrBridgeSearchFailed = errors.New("bridge search failed") var ErrAddressOnlyDryRunnable = errors.New("this address may only be used for a dry run") var ErrCannotForwardRequest = errors.New("driver is not able to forward requests") +var ErrCannotForget = errors.New("forget not supported on this bridge type") var ErrInvalidAddress = errors.New("invalid mac address") var ErrPayloadTooShort = errors.New("payload too short")