Co-authored-by: Gisle Aune <dev@gisle.me> Reviewed-on: #1 Co-authored-by: gisle <gisle@hidden-email> Co-committed-by: gisle <gisle@hidden-email>
This commit was merged in pull request #1.
This commit is contained in:
+95
-12
@@ -3,9 +3,11 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/lucifer/new-server/app/config"
|
||||
"git.aiterp.net/lucifer/new-server/app/services/publisher"
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
func fetchDevices(ctx context.Context, fetchStr string) ([]models.Device, error) {
|
||||
@@ -15,11 +17,21 @@ func fetchDevices(ctx context.Context, fetchStr string) ([]models.Device, error)
|
||||
|
||||
func Devices(r gin.IRoutes) {
|
||||
r.GET("", handler(func(c *gin.Context) (interface{}, error) {
|
||||
return config.DeviceRepository().FetchByReference(ctxOf(c), models.RKAll, "")
|
||||
devices, err := config.DeviceRepository().FetchByReference(ctxOf(c), models.RKAll, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return withSceneState(devices), nil
|
||||
}))
|
||||
|
||||
r.GET("/:fetch", handler(func(c *gin.Context) (interface{}, error) {
|
||||
return fetchDevices(ctxOf(c), c.Param("fetch"))
|
||||
devices, err := fetchDevices(ctxOf(c), c.Param("fetch"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return withSceneState(devices), nil
|
||||
}))
|
||||
|
||||
r.PUT("", handler(func(c *gin.Context) (interface{}, error) {
|
||||
@@ -70,7 +82,7 @@ func Devices(r gin.IRoutes) {
|
||||
}
|
||||
}()
|
||||
|
||||
return changed, nil
|
||||
return withSceneState(changed), nil
|
||||
}))
|
||||
|
||||
r.PUT("/:fetch", handler(func(c *gin.Context) (interface{}, error) {
|
||||
@@ -98,7 +110,7 @@ func Devices(r gin.IRoutes) {
|
||||
}
|
||||
}
|
||||
|
||||
return devices, nil
|
||||
return withSceneState(devices), nil
|
||||
}))
|
||||
|
||||
r.PUT("/:fetch/state", handler(func(c *gin.Context) (interface{}, error) {
|
||||
@@ -126,16 +138,13 @@ func Devices(r gin.IRoutes) {
|
||||
config.PublishChannel <- devices
|
||||
|
||||
go func() {
|
||||
for _, device := range devices {
|
||||
err := config.DeviceRepository().Save(context.Background(), &device, models.SMState)
|
||||
if err != nil {
|
||||
log.Println("Failed to save device for state:", err)
|
||||
continue
|
||||
}
|
||||
err = config.DeviceRepository().SaveMany(ctxOf(c), models.SMState, devices)
|
||||
if err != nil {
|
||||
log.Println("Failed to save devices states")
|
||||
}
|
||||
}()
|
||||
|
||||
return devices, nil
|
||||
return withSceneState(devices), nil
|
||||
}))
|
||||
|
||||
r.PUT("/:fetch/tags", handler(func(c *gin.Context) (interface{}, error) {
|
||||
@@ -192,6 +201,80 @@ func Devices(r gin.IRoutes) {
|
||||
}
|
||||
}
|
||||
|
||||
return devices, nil
|
||||
return withSceneState(devices), nil
|
||||
}))
|
||||
|
||||
r.PUT("/:fetch/scene", handler(func(c *gin.Context) (interface{}, error) {
|
||||
var body models.DeviceSceneAssignment
|
||||
err := parseBody(c, &body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
devices, err := fetchDevices(ctxOf(c), c.Param("fetch"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(devices) == 0 {
|
||||
return []models.Device{}, nil
|
||||
}
|
||||
|
||||
_, err = config.SceneRepository().Find(ctxOf(c), body.SceneID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if body.DurationMS < 0 {
|
||||
body.DurationMS = 0
|
||||
}
|
||||
body.StartTime = time.Now()
|
||||
|
||||
pushMode := c.Query("push") == "true"
|
||||
for i := range devices {
|
||||
if pushMode {
|
||||
devices[i].SceneAssignments = append(devices[i].SceneAssignments, body)
|
||||
} else {
|
||||
devices[i].SceneAssignments = []models.DeviceSceneAssignment{body}
|
||||
}
|
||||
}
|
||||
config.PublishChannel <- devices
|
||||
|
||||
err = config.DeviceRepository().SaveMany(ctxOf(c), 0, devices)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return withSceneState(devices), nil
|
||||
}))
|
||||
|
||||
r.DELETE("/:fetch/scene", handler(func(c *gin.Context) (interface{}, error) {
|
||||
devices, err := fetchDevices(ctxOf(c), c.Param("fetch"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(devices) == 0 {
|
||||
return []models.Device{}, nil
|
||||
}
|
||||
|
||||
for i := range devices {
|
||||
devices[i].SceneAssignments = []models.DeviceSceneAssignment{}
|
||||
}
|
||||
config.PublishChannel <- devices
|
||||
|
||||
err = config.DeviceRepository().SaveMany(ctxOf(c), 0, devices)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return withSceneState(devices), nil
|
||||
}))
|
||||
}
|
||||
|
||||
func withSceneState(devices []models.Device) []models.Device {
|
||||
res := make([]models.Device, 0, len(devices))
|
||||
for _, device := range devices {
|
||||
device.SceneState = publisher.Global().SceneState(device.ID)
|
||||
res = append(res, device)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"git.aiterp.net/lucifer/new-server/app/config"
|
||||
"git.aiterp.net/lucifer/new-server/app/services/publisher"
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func Scenes(r gin.IRoutes) {
|
||||
r.GET("", handler(func(c *gin.Context) (interface{}, error) {
|
||||
return config.SceneRepository().FetchAll(ctxOf(c))
|
||||
}))
|
||||
|
||||
r.GET("/:id", handler(func(c *gin.Context) (interface{}, error) {
|
||||
return config.SceneRepository().Find(ctxOf(c), intParam(c, "id"))
|
||||
}))
|
||||
|
||||
r.POST("", handler(func(c *gin.Context) (interface{}, error) {
|
||||
var body models.Scene
|
||||
err := parseBody(c, &body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = body.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = config.SceneRepository().Save(ctxOf(c), &body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
publisher.Global().UpdateScene(body)
|
||||
|
||||
return body, nil
|
||||
}))
|
||||
|
||||
r.PUT("/:id", handler(func(c *gin.Context) (interface{}, error) {
|
||||
var body models.Scene
|
||||
err := parseBody(c, &body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scene, err := config.SceneRepository().Find(ctxOf(c), intParam(c, "id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body.ID = scene.ID
|
||||
err = body.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = config.SceneRepository().Save(ctxOf(c), &body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
publisher.Global().UpdateScene(body)
|
||||
|
||||
return body, nil
|
||||
}))
|
||||
}
|
||||
@@ -15,6 +15,13 @@ var errorMap = map[error]int{
|
||||
models.ErrBadColor: 400,
|
||||
models.ErrInternal: 500,
|
||||
models.ErrUnknownColorFormat: 400,
|
||||
|
||||
models.ErrSceneInvalidInterval: 400,
|
||||
models.ErrSceneNoRoles: 400,
|
||||
models.ErrSceneRoleNoStates: 400,
|
||||
models.ErrSceneRoleUnsupportedOrdering: 422,
|
||||
models.ErrSceneRoleUnknownEffect: 422,
|
||||
models.ErrSceneRoleUnknownPowerMode: 422,
|
||||
}
|
||||
|
||||
type response struct {
|
||||
|
||||
@@ -60,6 +60,31 @@ func (client *Client) PutDeviceTags(ctx context.Context, fetchStr string, addTag
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
func (client *Client) AssignDevice(ctx context.Context, fetchStr string, push bool, assignment models.DeviceSceneAssignment) ([]models.Device, error) {
|
||||
query := ""
|
||||
if push {
|
||||
query = "?push=true"
|
||||
}
|
||||
|
||||
devices := make([]models.Device, 0, 16)
|
||||
err := client.Fetch(ctx, "PUT", "/api/devices/"+fetchStr+"/scene"+query, &devices, assignment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
func (client *Client) ClearDevice(ctx context.Context, fetchStr string) ([]models.Device, error) {
|
||||
devices := make([]models.Device, 0, 16)
|
||||
err := client.Fetch(ctx, "DELETE", "/api/devices/"+fetchStr+"/scene", &devices, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
func (client *Client) FireEvent(ctx context.Context, event models.Event) error {
|
||||
err := client.Fetch(ctx, "POST", "/api/events", nil, event)
|
||||
if err != nil {
|
||||
|
||||
@@ -28,7 +28,7 @@ func (client *Client) GetHandler(ctx context.Context, id int) (*models.EventHand
|
||||
|
||||
func (client *Client) PutHandler(ctx context.Context, handler *models.EventHandler) (*models.EventHandler, error) {
|
||||
var response models.EventHandler
|
||||
err := client.Fetch(ctx, "PUT", fmt.Sprintf("/api/event-handlers/%d", handler.ID), &response, handler)
|
||||
err := client.Fetch(ctx, "PUT", fmt.Sprintf("/api/event-handlers/%d?hard=true", handler.ID), &response, handler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
)
|
||||
|
||||
func (client *Client) GetScenes(ctx context.Context) ([]models.Scene, error) {
|
||||
scenes := make([]models.Scene, 0, 16)
|
||||
err := client.Fetch(ctx, "GET", "/api/scenes", &scenes, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return scenes, nil
|
||||
}
|
||||
+3
-2
@@ -24,8 +24,9 @@ func DBX() *sqlx.DB {
|
||||
MySqlSchema(),
|
||||
))
|
||||
|
||||
dbx.SetMaxIdleConns(50)
|
||||
dbx.SetMaxOpenConns(100)
|
||||
dbx.SetMaxIdleConns(20)
|
||||
dbx.SetMaxOpenConns(40)
|
||||
dbx.SetConnMaxLifetime(0)
|
||||
}
|
||||
|
||||
return dbx
|
||||
|
||||
@@ -20,3 +20,7 @@ func DeviceRepository() models.DeviceRepository {
|
||||
func EventHandlerRepository() models.EventHandlerRepository {
|
||||
return &mysql.EventHandlerRepo{DBX: DBX()}
|
||||
}
|
||||
|
||||
func SceneRepository() models.SceneRepository {
|
||||
return &mysql.SceneRepo{DBX: DBX()}
|
||||
}
|
||||
|
||||
+13
-1
@@ -1,17 +1,28 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.aiterp.net/lucifer/new-server/app/api"
|
||||
"git.aiterp.net/lucifer/new-server/app/config"
|
||||
"git.aiterp.net/lucifer/new-server/app/services"
|
||||
"git.aiterp.net/lucifer/new-server/app/services/publisher"
|
||||
"github.com/gin-gonic/gin"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
func StartServer() {
|
||||
setupCtx, cancel := context.WithTimeout(context.Background(), time.Second * 10)
|
||||
defer cancel()
|
||||
|
||||
err := publisher.Initialize(setupCtx)
|
||||
if err != nil {
|
||||
log.Fatalln("Publish init failed:", err)
|
||||
return
|
||||
}
|
||||
|
||||
services.StartEventHandler()
|
||||
services.StartPublisher()
|
||||
services.ConnectToBridges()
|
||||
services.CheckNewDevices()
|
||||
|
||||
@@ -25,6 +36,7 @@ func StartServer() {
|
||||
api.DriverKinds(apiGin.Group("/driver-kinds"))
|
||||
api.Events(apiGin.Group("/events"))
|
||||
api.EventHandlers(apiGin.Group("/event-handlers"))
|
||||
api.Scenes(apiGin.Group("/scenes"))
|
||||
|
||||
log.Fatal(ginny.Run(fmt.Sprintf("0.0.0.0:%d", config.ServerPort())))
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"git.aiterp.net/lucifer/new-server/app/config"
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"log"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -53,15 +52,6 @@ func runConnectToBridges() error {
|
||||
log.Printf("Running bridge \"%s\" (%d)", bridge.Name, bridge.ID)
|
||||
|
||||
go func(bridge models.Bridge, cancel func()) {
|
||||
savedDevices, err := config.DeviceRepository().FetchByReference(ctx, models.RKBridgeID, strconv.Itoa(bridge.ID))
|
||||
if err != nil {
|
||||
log.Println("Failed to fetch devices from db for refresh:", err)
|
||||
}
|
||||
err = driver.Publish(ctx, bridge, savedDevices)
|
||||
if err != nil {
|
||||
log.Println("Failed to publish devices from db before run:", err)
|
||||
}
|
||||
|
||||
err = driver.Run(ctx, bridge, config.EventChannel)
|
||||
log.Printf("Bridge \"%s\" (%d) stopped: %s", bridge.Name, bridge.ID, err)
|
||||
|
||||
|
||||
+16
-6
@@ -155,23 +155,33 @@ func handleEvent(event models.Event) (responses []models.Event) {
|
||||
if err != nil {
|
||||
log.Println("Error updating state for device", device.ID, "err:", err)
|
||||
}
|
||||
|
||||
if action.SetScene != nil {
|
||||
action.SetScene.StartTime = time.Now()
|
||||
allDevices[i].SceneAssignments = []models.DeviceSceneAssignment{*action.SetScene}
|
||||
}
|
||||
if action.PushScene != nil {
|
||||
action.PushScene.StartTime = time.Now()
|
||||
allDevices[i].SceneAssignments = append(allDevices[i].SceneAssignments, *action.PushScene)
|
||||
}
|
||||
}
|
||||
|
||||
config.PublishChannel <- allDevices
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
for _, device := range allDevices {
|
||||
wg.Add(1)
|
||||
|
||||
go func(device models.Device) {
|
||||
err := config.DeviceRepository().Save(context.Background(), &device, models.SMState)
|
||||
if len(allDevices) > 0 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
err := config.DeviceRepository().SaveMany(context.Background(), models.SMState, allDevices)
|
||||
if err != nil {
|
||||
log.Println("Failed to save device for state:", err)
|
||||
log.Println("Failed to save devices' state:", err)
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}(device)
|
||||
}()
|
||||
}
|
||||
|
||||
for _, handler := range deadHandlers {
|
||||
wg.Add(1)
|
||||
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/lucifer/new-server/app/config"
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func StartPublisher() {
|
||||
ctx := context.Background()
|
||||
|
||||
go func() {
|
||||
for devices := range config.PublishChannel {
|
||||
if len(devices) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
lists := make(map[int][]models.Device, 4)
|
||||
for _, device := range devices {
|
||||
lists[device.BridgeID] = append(lists[device.BridgeID], device)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Second * 30)
|
||||
|
||||
bridges, err := config.BridgeRepository().FetchAll(ctx)
|
||||
if err != nil {
|
||||
log.Println("Publishing error (1): " + err.Error())
|
||||
cancel()
|
||||
continue
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
for _, devices := range lists {
|
||||
wg.Add(1)
|
||||
|
||||
go func(devices []models.Device) {
|
||||
defer wg.Done()
|
||||
|
||||
var bridge models.Bridge
|
||||
for _, bridge2 := range bridges {
|
||||
if bridge2.ID == devices[0].BridgeID {
|
||||
bridge = bridge2
|
||||
}
|
||||
}
|
||||
if bridge.ID == 0 {
|
||||
log.Println("Unknown bridge")
|
||||
}
|
||||
|
||||
bridge, err := config.BridgeRepository().Find(ctx, devices[0].BridgeID)
|
||||
if err != nil {
|
||||
log.Println("Publishing error (1): " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
driver, err := config.DriverProvider().Provide(bridge.Driver)
|
||||
if err != nil {
|
||||
log.Println("Publishing error (2): " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = driver.Publish(ctx, bridge, devices)
|
||||
if err != nil {
|
||||
log.Println("Publishing error (3): " + err.Error())
|
||||
return
|
||||
}
|
||||
}(devices)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
package publisher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/lucifer/new-server/app/config"
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Publisher struct {
|
||||
mu sync.Mutex
|
||||
sceneData map[int]*models.Scene
|
||||
scenes []*Scene
|
||||
sceneAssignment map[int]*Scene
|
||||
started map[int]bool
|
||||
pending map[int][]models.Device
|
||||
waiting map[int]chan struct{}
|
||||
}
|
||||
|
||||
func (p *Publisher) SceneState(deviceID int) *models.DeviceState {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if s := p.sceneAssignment[deviceID]; s != nil {
|
||||
return s.LastState(deviceID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Publisher) UpdateScene(data models.Scene) {
|
||||
p.mu.Lock()
|
||||
p.sceneData[data.ID] = &data
|
||||
|
||||
for _, scene := range p.scenes {
|
||||
if scene.data.ID == data.ID {
|
||||
scene.UpdateScene(data)
|
||||
}
|
||||
}
|
||||
p.mu.Unlock()
|
||||
}
|
||||
|
||||
func (p *Publisher) ReloadScenes(ctx context.Context) error {
|
||||
scenes, err := config.SceneRepository().FetchAll(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
for i, scene := range scenes {
|
||||
p.sceneData[scene.ID] = &scenes[i]
|
||||
}
|
||||
|
||||
for _, scene := range p.scenes {
|
||||
scene.UpdateScene(*p.sceneData[scene.data.ID])
|
||||
}
|
||||
p.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Publisher) ReloadDevices(ctx context.Context) error {
|
||||
devices, err := config.DeviceRepository().FetchByReference(ctx, models.RKAll, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.Publish(devices...)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Publisher) Publish(devices ...models.Device) {
|
||||
if len(devices) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
for _, device := range devices {
|
||||
if !p.started[device.BridgeID] {
|
||||
p.started[device.BridgeID] = true
|
||||
go p.runBridge(device.BridgeID)
|
||||
}
|
||||
|
||||
p.reassignDevice(device)
|
||||
|
||||
if p.sceneAssignment[device.ID] != nil {
|
||||
p.sceneAssignment[device.ID].UpsertDevice(device)
|
||||
} else {
|
||||
p.pending[device.BridgeID] = append(p.pending[device.BridgeID], device)
|
||||
if p.waiting[device.BridgeID] != nil {
|
||||
close(p.waiting[device.BridgeID])
|
||||
p.waiting[device.BridgeID] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Publisher) PublishChannel(ch <-chan []models.Device) {
|
||||
for list := range ch {
|
||||
p.Publish(list...)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Publisher) Run() {
|
||||
ticker := time.NewTicker(time.Millisecond * 100)
|
||||
deleteList := make([]int, 0, 8)
|
||||
updatedList := make([]models.Device, 0, 16)
|
||||
|
||||
for range ticker.C {
|
||||
deleteList = deleteList[:0]
|
||||
updatedList = updatedList[:0]
|
||||
|
||||
p.mu.Lock()
|
||||
for i, scene := range p.scenes {
|
||||
if (!scene.endTime.IsZero() && time.Now().After(scene.endTime)) || scene.Empty() {
|
||||
deleteList = append(deleteList, i-len(deleteList))
|
||||
updatedList = append(updatedList, scene.AllDevices()...)
|
||||
|
||||
log.Printf("Removing scene instance for %s (%d)", scene.data.Name, scene.data.ID)
|
||||
|
||||
for _, device := range scene.AllDevices() {
|
||||
p.sceneAssignment[device.ID] = nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if scene.Due() {
|
||||
updatedList = append(updatedList, scene.Run()...)
|
||||
updatedList = append(updatedList, scene.UnaffectedDevices()...)
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range deleteList {
|
||||
p.scenes = append(p.scenes[:i], p.scenes[i+1:]...)
|
||||
}
|
||||
|
||||
for _, device := range updatedList {
|
||||
if !p.started[device.BridgeID] {
|
||||
p.started[device.BridgeID] = true
|
||||
go p.runBridge(device.BridgeID)
|
||||
}
|
||||
|
||||
p.pending[device.BridgeID] = append(p.pending[device.BridgeID], device)
|
||||
if p.waiting[device.BridgeID] != nil {
|
||||
close(p.waiting[device.BridgeID])
|
||||
p.waiting[device.BridgeID] = nil
|
||||
}
|
||||
}
|
||||
p.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// reassignDevice re-evaluates the device's scene assignment config. It will return whether the scene changed, which
|
||||
// should trigger an update.
|
||||
func (p *Publisher) reassignDevice(device models.Device) bool {
|
||||
var selectedAssignment *models.DeviceSceneAssignment
|
||||
for _, assignment := range device.SceneAssignments {
|
||||
duration := time.Duration(assignment.DurationMS) * time.Millisecond
|
||||
if duration <= 0 || time.Now().Before(assignment.StartTime.Add(duration)) {
|
||||
selectedAssignment = &assignment
|
||||
}
|
||||
}
|
||||
|
||||
if selectedAssignment == nil {
|
||||
if p.sceneAssignment[device.ID] != nil {
|
||||
p.sceneAssignment[device.ID].RemoveDevice(device)
|
||||
delete(p.sceneAssignment, device.ID)
|
||||
|
||||
// Scene changed
|
||||
return true
|
||||
}
|
||||
|
||||
// Stop here, no scene should be assigned.
|
||||
return false
|
||||
} else {
|
||||
if p.sceneData[selectedAssignment.SceneID] == nil {
|
||||
// Freeze until scene becomes available.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if p.sceneAssignment[device.ID] != nil {
|
||||
scene := p.sceneAssignment[device.ID]
|
||||
if scene.data.ID == selectedAssignment.SceneID && scene.group == selectedAssignment.Group {
|
||||
// Current assignment is good.
|
||||
return false
|
||||
}
|
||||
|
||||
p.sceneAssignment[device.ID].RemoveDevice(device)
|
||||
delete(p.sceneAssignment, device.ID)
|
||||
}
|
||||
|
||||
for _, scene := range p.scenes {
|
||||
if scene.data.ID == selectedAssignment.SceneID && scene.group == selectedAssignment.Group {
|
||||
p.sceneAssignment[device.ID] = scene
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
newScene := &Scene{
|
||||
data: p.sceneData[selectedAssignment.SceneID],
|
||||
group: selectedAssignment.Group,
|
||||
startTime: selectedAssignment.StartTime,
|
||||
endTime: selectedAssignment.StartTime.Add(time.Duration(selectedAssignment.DurationMS) * time.Millisecond),
|
||||
roleMap: make(map[int]int, 16),
|
||||
roleList: make(map[int][]models.Device, 16),
|
||||
lastStates: make(map[int]models.DeviceState, 16),
|
||||
due: true,
|
||||
}
|
||||
p.sceneAssignment[device.ID] = newScene
|
||||
p.scenes = append(p.scenes, newScene)
|
||||
|
||||
if selectedAssignment.DurationMS <= 0 {
|
||||
newScene.endTime = time.Time{}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Publisher) runBridge(id int) {
|
||||
defer func() {
|
||||
p.mu.Lock()
|
||||
p.started[id] = false
|
||||
p.mu.Unlock()
|
||||
}()
|
||||
|
||||
bridge, err := config.BridgeRepository().Find(context.Background(), id)
|
||||
if err != nil {
|
||||
log.Println("Failed to get bridge data:", err)
|
||||
return
|
||||
}
|
||||
|
||||
driver, err := config.DriverProvider().Provide(bridge.Driver)
|
||||
if err != nil {
|
||||
log.Println("Failed to get bridge driver:", err)
|
||||
log.Println("Maybe Lucifer needs to be updated.")
|
||||
return
|
||||
}
|
||||
|
||||
devices := make(map[int]models.Device)
|
||||
|
||||
for {
|
||||
p.mu.Lock()
|
||||
if len(p.pending[id]) == 0 {
|
||||
if p.waiting[id] == nil {
|
||||
p.waiting[id] = make(chan struct{})
|
||||
}
|
||||
waitCh := p.waiting[id]
|
||||
p.mu.Unlock()
|
||||
<-waitCh
|
||||
p.mu.Lock()
|
||||
}
|
||||
|
||||
updates := p.pending[id]
|
||||
p.pending[id] = p.pending[id][:0:0]
|
||||
p.mu.Unlock()
|
||||
|
||||
// Only allow the latest update per device (this avoids slow bridges causing a backlog of cations).
|
||||
for key := range devices {
|
||||
delete(devices, key)
|
||||
}
|
||||
for _, update := range updates {
|
||||
devices[update.ID] = update
|
||||
}
|
||||
updates = updates[:0]
|
||||
for _, value := range devices {
|
||||
updates = append(updates, value)
|
||||
}
|
||||
|
||||
err := driver.Publish(context.Background(), bridge, updates)
|
||||
if err != nil {
|
||||
log.Println("Failed to publish to driver:", err)
|
||||
|
||||
p.mu.Lock()
|
||||
p.pending[id] = append(updates, p.pending[id]...)
|
||||
p.mu.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var publisher = Publisher{
|
||||
sceneData: make(map[int]*models.Scene),
|
||||
scenes: make([]*Scene, 0, 16),
|
||||
sceneAssignment: make(map[int]*Scene, 16),
|
||||
started: make(map[int]bool),
|
||||
pending: make(map[int][]models.Device),
|
||||
waiting: make(map[int]chan struct{}),
|
||||
}
|
||||
|
||||
func Global() *Publisher {
|
||||
return &publisher
|
||||
}
|
||||
|
||||
func Initialize(ctx context.Context) error {
|
||||
err := publisher.ReloadScenes(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = publisher.ReloadDevices(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go publisher.Run()
|
||||
time.Sleep(time.Millisecond * 50)
|
||||
go publisher.PublishChannel(config.PublishChannel)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package publisher
|
||||
|
||||
import (
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Scene struct {
|
||||
data *models.Scene
|
||||
group string
|
||||
startTime time.Time
|
||||
endTime time.Time
|
||||
roleMap map[int]int
|
||||
roleList map[int][]models.Device
|
||||
lastStates map[int]models.DeviceState
|
||||
|
||||
due bool
|
||||
lastInterval int64
|
||||
}
|
||||
|
||||
// UpdateScene updates the scene data and re-seats all devices.
|
||||
func (s *Scene) UpdateScene(data models.Scene) {
|
||||
devices := make([]models.Device, 0, 16)
|
||||
|
||||
// Collect all devices into the undefined role (-1)
|
||||
for _, list := range s.roleList {
|
||||
for _, device := range list {
|
||||
devices = append(devices, device)
|
||||
s.roleMap[device.ID] = -1
|
||||
}
|
||||
}
|
||||
s.roleList = map[int][]models.Device{-1: devices}
|
||||
|
||||
// Update data and reset devices.
|
||||
s.data = &data
|
||||
for _, device := range append(devices[:0:0], devices...) {
|
||||
s.UpsertDevice(device)
|
||||
}
|
||||
}
|
||||
|
||||
// UpsertDevice moves the device if necessary and updates its state.
|
||||
func (s *Scene) UpsertDevice(device models.Device) {
|
||||
if s.data == nil {
|
||||
s.roleMap[device.ID] = -1
|
||||
s.roleList[-1] = append(s.roleList[-1], device)
|
||||
return
|
||||
}
|
||||
|
||||
oldIndex, hasOldIndex := s.roleMap[device.ID]
|
||||
newIndex := s.data.RoleIndex(&device)
|
||||
|
||||
s.roleMap[device.ID] = newIndex
|
||||
|
||||
if hasOldIndex {
|
||||
for i, device2 := range s.roleList[oldIndex] {
|
||||
if device2.ID == device.ID {
|
||||
s.roleList[oldIndex] = append(s.roleList[oldIndex][:i], s.roleList[oldIndex][i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.due = true
|
||||
|
||||
s.roleList[newIndex] = append(s.roleList[newIndex], device)
|
||||
if newIndex != -1 {
|
||||
s.data.Roles[newIndex].ApplyOrder(s.roleList[newIndex])
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveDevice finds and remove a device. It's a noop if the device does not exist in this scene.
|
||||
func (s *Scene) RemoveDevice(device models.Device) {
|
||||
roleIndex, hasRoleIndex := s.roleMap[device.ID]
|
||||
if !hasRoleIndex {
|
||||
return
|
||||
}
|
||||
|
||||
for i, device2 := range s.roleList[roleIndex] {
|
||||
if device2.ID == device.ID {
|
||||
s.roleList[roleIndex] = append(s.roleList[roleIndex][:i], s.roleList[roleIndex][i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
s.due = true
|
||||
|
||||
delete(s.roleMap, device.ID)
|
||||
delete(s.lastStates, device.ID)
|
||||
}
|
||||
|
||||
func (s *Scene) Empty() bool {
|
||||
for _, list := range s.roleList {
|
||||
if len(list) > 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Scene) Due() bool {
|
||||
if s.due {
|
||||
return true
|
||||
}
|
||||
|
||||
if s.data.IntervalMS > 0 {
|
||||
interval := time.Duration(s.data.IntervalMS) * time.Millisecond
|
||||
return int64(time.Since(s.startTime)/interval) != s.lastInterval
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Scene) UnaffectedDevices() []models.Device {
|
||||
return append(s.roleList[-1][:0:0], s.roleList[-1]...)
|
||||
}
|
||||
|
||||
func (s *Scene) AllDevices() []models.Device {
|
||||
res := make([]models.Device, 0, 16)
|
||||
for _, list := range s.roleList {
|
||||
res = append(res, list...)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// Run runs the scene
|
||||
func (s *Scene) Run() []models.Device {
|
||||
if s.data == nil {
|
||||
return []models.Device{}
|
||||
}
|
||||
|
||||
intervalNumber := int64(0)
|
||||
intervalMax := int64(1)
|
||||
if s.data.IntervalMS > 0 {
|
||||
interval := time.Duration(s.data.IntervalMS) * time.Millisecond
|
||||
intervalNumber = int64(time.Since(s.startTime) / interval)
|
||||
|
||||
if !s.endTime.IsZero() {
|
||||
intervalMax = int64(s.endTime.Sub(s.startTime) / interval)
|
||||
} else {
|
||||
intervalMax = intervalNumber + 1
|
||||
}
|
||||
}
|
||||
|
||||
updatedDevices := make([]models.Device, 0, 16)
|
||||
for i, list := range s.roleList {
|
||||
if i == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
role := s.data.Roles[i]
|
||||
|
||||
for j, device := range list {
|
||||
newState := role.ApplyEffect(&device, models.SceneRunContext{
|
||||
Index: j,
|
||||
Length: len(list),
|
||||
IntervalNumber: intervalNumber,
|
||||
IntervalMax: intervalMax,
|
||||
})
|
||||
|
||||
err := device.SetState(newState)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
s.lastStates[device.ID] = device.State
|
||||
|
||||
updatedDevices = append(updatedDevices, device)
|
||||
}
|
||||
}
|
||||
|
||||
s.due = false
|
||||
s.lastInterval = intervalNumber
|
||||
|
||||
return updatedDevices
|
||||
}
|
||||
|
||||
func (s *Scene) LastState(id int) *models.DeviceState {
|
||||
lastState, ok := s.lastStates[id]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &lastState
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Param struct {
|
||||
@@ -221,6 +222,17 @@ func (p Params) DeviceState(prefix string) models.NewDeviceState {
|
||||
}
|
||||
}
|
||||
|
||||
func (p Params) SceneAssignment(prefix string) *models.DeviceSceneAssignment {
|
||||
if p.Get(prefix+"scene") == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &models.DeviceSceneAssignment{
|
||||
SceneID: p.Get(prefix+"scene").IntOr(-1),
|
||||
Group: p.Get(prefix+"scene.group").StringOr(time.Now().Format(time.RFC3339)),
|
||||
DurationMS: int64(p.Get(prefix+"scene.duration").IntOr(0)),
|
||||
}
|
||||
}
|
||||
|
||||
type Command struct {
|
||||
Name string
|
||||
|
||||
@@ -99,7 +99,7 @@ func handlerCmd(
|
||||
func applyCmdToHandler(model models.EventHandler, cmd Command) models.EventHandler {
|
||||
// Remove keys
|
||||
for _, elem := range cmd.Params.Strings(1) {
|
||||
if elem[0] != '-' {
|
||||
if !strings.HasPrefix(elem, "-") {
|
||||
continue
|
||||
}
|
||||
keyToRemove := elem[1:]
|
||||
@@ -128,6 +128,10 @@ func applyCmdToHandler(model models.EventHandler, cmd Command) models.EventHandl
|
||||
model.From = models.Never
|
||||
case "to":
|
||||
model.To = models.Never
|
||||
case "set-scene":
|
||||
model.Actions.SetScene = nil
|
||||
case "push-scene":
|
||||
model.Actions.PushScene = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,5 +177,9 @@ func applyCmdToHandler(model models.EventHandler, cmd Command) models.EventHandl
|
||||
}
|
||||
}
|
||||
|
||||
// Scenes
|
||||
model.Actions.SetScene = cmd.Params.SceneAssignment("set-")
|
||||
model.Actions.PushScene = cmd.Params.SceneAssignment("push-")
|
||||
|
||||
return model
|
||||
}
|
||||
|
||||
@@ -96,6 +96,9 @@ func main() {
|
||||
case "handler":
|
||||
handlerCmd(ctx, c)
|
||||
|
||||
case "scene":
|
||||
sceneCmd(ctx, c)
|
||||
|
||||
// EVENT
|
||||
case "run":
|
||||
{
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/lucifer/new-server/app/client"
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"gopkg.in/yaml.v2"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func sceneCmd(
|
||||
ctx context.Context,
|
||||
c client.Client,
|
||||
) {
|
||||
cmd := parseCommand(os.Args[2:])
|
||||
|
||||
switch cmd.Name {
|
||||
case "create", "update":
|
||||
{
|
||||
fileName := cmd.Params.Get(0).String()
|
||||
if fileName == nil {
|
||||
log.Fatalln("Missing filename")
|
||||
}
|
||||
|
||||
file, err := os.Open(*fileName)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to open file:", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
yamlData := make(map[string]interface{})
|
||||
err = yaml.NewDecoder(file).Decode(yamlData)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to decode file:", err)
|
||||
return
|
||||
}
|
||||
|
||||
yamlData = camelCasify(yamlData)
|
||||
name, nameOk := yamlData["name"]
|
||||
if !nameOk {
|
||||
log.Fatalln("Missing name in yaml data.")
|
||||
}
|
||||
|
||||
var scene models.Scene
|
||||
if cmd.Name == "create" {
|
||||
err := c.Fetch(ctx, "POST", "/api/scenes", &scene, yamlData)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to create scene:", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
scenes, err := c.GetScenes(ctx)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to fetch existing scenes:", err)
|
||||
return
|
||||
}
|
||||
|
||||
id := -1
|
||||
for _, scene := range scenes {
|
||||
if scene.Name == name {
|
||||
id = scene.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
if id == -1 {
|
||||
log.Fatalln("Could not find scene with name", name)
|
||||
return
|
||||
}
|
||||
|
||||
err = c.Fetch(ctx, "PUT", "/api/scenes/"+strconv.Itoa(id), &scene, yamlData)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to update scene:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case "push", "assign":
|
||||
{
|
||||
fetch := cmd.Params.Get(0).String()
|
||||
id := cmd.Params.Get(1).Int()
|
||||
if fetch == nil || id == nil {
|
||||
log.Println("Usage: lucy scene assign <fetch> <id> <group=S> <duration=I>")
|
||||
}
|
||||
|
||||
devices, err := c.AssignDevice(ctx, *fetch, cmd.Name == "push", models.DeviceSceneAssignment{
|
||||
SceneID: *id,
|
||||
Group: cmd.Params.Get("group").StringOr(*fetch),
|
||||
DurationMS: int64(cmd.Params.Get("duration").IntOr(0)),
|
||||
})
|
||||
if err != nil {
|
||||
log.Println("Could not assign devices:", err)
|
||||
return
|
||||
}
|
||||
|
||||
WriteDeviceInfoTable(os.Stdout, devices)
|
||||
}
|
||||
|
||||
case "clear":
|
||||
fetch := cmd.Params.Get(0).String()
|
||||
if fetch == nil {
|
||||
log.Println("Usage: lucy scene clear <fetch>")
|
||||
}
|
||||
|
||||
devices, err := c.ClearDevice(ctx, *fetch)
|
||||
if err != nil {
|
||||
log.Println("Could not clear devices:", err)
|
||||
return
|
||||
}
|
||||
|
||||
WriteDeviceInfoTable(os.Stdout, devices)
|
||||
}
|
||||
}
|
||||
|
||||
func camelCasify(m map[string]interface{}) map[string]interface{} {
|
||||
m2 := make(map[string]interface{}, len(m))
|
||||
for key, value := range m {
|
||||
b := strings.Builder{}
|
||||
snake := false
|
||||
for _, ch := range key {
|
||||
if ch == '_' {
|
||||
snake = true
|
||||
} else if snake {
|
||||
b.WriteRune(unicode.ToUpper(ch))
|
||||
snake = false
|
||||
} else {
|
||||
b.WriteRune(ch)
|
||||
}
|
||||
}
|
||||
|
||||
switch value := value.(type) {
|
||||
case []interface{}:
|
||||
valueCopy := make([]interface{}, len(value))
|
||||
for i, elem := range value {
|
||||
switch elem := elem.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
m3 := make(map[string]interface{})
|
||||
for k, v := range elem {
|
||||
if kStr, ok := k.(string); ok {
|
||||
m3[kStr] = v
|
||||
}
|
||||
}
|
||||
|
||||
valueCopy[i] = camelCasify(m3)
|
||||
case map[string]interface{}:
|
||||
valueCopy[i] = camelCasify(elem)
|
||||
default:
|
||||
valueCopy[i] = elem
|
||||
}
|
||||
}
|
||||
|
||||
m2[b.String()] = valueCopy
|
||||
case map[string]interface{}:
|
||||
m2[b.String()] = camelCasify(value)
|
||||
default:
|
||||
m2[b.String()] = value
|
||||
}
|
||||
}
|
||||
|
||||
return m2
|
||||
}
|
||||
+34
-3
@@ -14,16 +14,32 @@ func WriteDeviceStateTable(w io.Writer, devices []models.Device) {
|
||||
table.SetHeader([]string{"ID", "NAME", "POWER", "COLOR", "INTENSITY", "TEMPERATURE"})
|
||||
|
||||
table.SetReflowDuringAutoWrap(true)
|
||||
table.SetColumnAlignment([]int{
|
||||
tablewriter.ALIGN_RIGHT,
|
||||
tablewriter.ALIGN_LEFT,
|
||||
tablewriter.ALIGN_RIGHT,
|
||||
tablewriter.ALIGN_LEFT,
|
||||
tablewriter.ALIGN_RIGHT,
|
||||
tablewriter.ALIGN_RIGHT,
|
||||
})
|
||||
|
||||
for _, v := range devices {
|
||||
powerStr := ""
|
||||
if v.HasCapability(models.DCPower) {
|
||||
powerStr = strconv.FormatBool(v.State.Power)
|
||||
if v.SceneState != nil && v.SceneState.Power != v.State.Power {
|
||||
powerStr = strconv.FormatBool(v.SceneState.Power) + "*"
|
||||
} else {
|
||||
powerStr = strconv.FormatBool(v.State.Power)
|
||||
}
|
||||
}
|
||||
|
||||
colorStr := ""
|
||||
if v.HasCapability(models.DCColorHSK, models.DCColorHS, models.DCColorKelvin) {
|
||||
colorStr = v.State.Color.String()
|
||||
if v.SceneState != nil && v.SceneState.Color.String() != v.State.Color.String() {
|
||||
colorStr = v.SceneState.Color.String() + "*"
|
||||
} else {
|
||||
colorStr = v.State.Color.String()
|
||||
}
|
||||
}
|
||||
|
||||
temperatureString := ""
|
||||
@@ -33,7 +49,11 @@ func WriteDeviceStateTable(w io.Writer, devices []models.Device) {
|
||||
|
||||
intensityString := ""
|
||||
if v.HasCapability(models.DCIntensity) {
|
||||
intensityString = strconv.FormatFloat(v.State.Intensity, 'f', -1, 64)
|
||||
if v.SceneState != nil && v.SceneState.Intensity != v.State.Intensity {
|
||||
intensityString = strconv.FormatFloat(v.SceneState.Intensity, 'g', 2, 64) + "*"
|
||||
} else {
|
||||
intensityString = strconv.FormatFloat(v.State.Intensity, 'f', -1, 64)
|
||||
}
|
||||
}
|
||||
|
||||
table.Append([]string{
|
||||
@@ -104,6 +124,17 @@ func WriteHandlerInfoTable(w io.Writer, handlers []models.EventHandler) {
|
||||
if h.Actions.FireEvent != nil {
|
||||
actionStr += fmt.Sprintf("fireEvent=%s ", (*h.Actions.FireEvent).Name)
|
||||
}
|
||||
if h.Actions.FireEvent != nil {
|
||||
actionStr += fmt.Sprintf("fireEvent=%s ", (*h.Actions.FireEvent).Name)
|
||||
}
|
||||
if h.Actions.SetScene != nil {
|
||||
s := h.Actions.SetScene
|
||||
actionStr += fmt.Sprintf("setScene=(id=%d,group=\"%s\",duration=%d) ", s.SceneID, s.Group, s.DurationMS)
|
||||
}
|
||||
if h.Actions.PushScene != nil {
|
||||
s := h.Actions.PushScene
|
||||
actionStr += fmt.Sprintf("pushScene=(id=%d,group=\"%s\",duration=%d) ", s.SceneID, s.Group, s.DurationMS)
|
||||
}
|
||||
|
||||
eventName := h.EventName
|
||||
if h.OneShot {
|
||||
|
||||
@@ -122,6 +122,9 @@ func (state *hueSensorState) Update(newData SensorData) *models.Event {
|
||||
if time.Since(stateTime) > time.Second*3 {
|
||||
return nil
|
||||
}
|
||||
if state.prevData == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
pe := state.prevData.State.ButtonEvent
|
||||
ce := newData.State.ButtonEvent
|
||||
|
||||
@@ -204,7 +204,9 @@ func (b *Bridge) Run(ctx context.Context, debug bool) error {
|
||||
b.checkAndUpdateState(state)
|
||||
}
|
||||
|
||||
if time.Since(state.discoveredTime) > time.Second*10 {
|
||||
if time.Since(state.discoveredTime) > time.Second*10 && time.Since(state.fwSpamTime) > time.Second * 30 {
|
||||
state.fwSpamTime = time.Now()
|
||||
|
||||
if state.firmware == nil {
|
||||
_, _ = client.Send(state.target, &GetHostFirmware{})
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ type State struct {
|
||||
lightStateTime time.Time
|
||||
requestTime time.Time
|
||||
updateTime time.Time
|
||||
fwSpamTime time.Time
|
||||
acksPending []uint8
|
||||
}
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ func (b *bridge) Update(devices []models.Device) {
|
||||
red, green, blue := color.RGB255()
|
||||
newColor := [4]byte{red, green, blue, 255}
|
||||
if newColor != panel.ColorRGBA {
|
||||
panel.update(newColor, time.Now().Add(time.Millisecond*120))
|
||||
panel.update(newColor, time.Now().Add(time.Millisecond*220))
|
||||
}
|
||||
} else {
|
||||
panel.update([4]byte{0, 0, 0, 0}, time.Now())
|
||||
|
||||
+117
-92
@@ -12,13 +12,14 @@ import (
|
||||
)
|
||||
|
||||
type deviceRecord struct {
|
||||
ID int `db:"id"`
|
||||
BridgeID int `db:"bridge_id"`
|
||||
InternalID string `db:"internal_id"`
|
||||
Icon string `db:"icon"`
|
||||
Name string `db:"name"`
|
||||
Capabilities string `db:"capabilities"`
|
||||
ButtonNames string `db:"button_names"`
|
||||
ID int `db:"id"`
|
||||
BridgeID int `db:"bridge_id"`
|
||||
InternalID string `db:"internal_id"`
|
||||
Icon string `db:"icon"`
|
||||
Name string `db:"name"`
|
||||
Capabilities string `db:"capabilities"`
|
||||
ButtonNames string `db:"button_names"`
|
||||
SceneAssignmentJSON json.RawMessage `db:"scene_assignments"`
|
||||
}
|
||||
|
||||
type deviceStateRecord struct {
|
||||
@@ -117,125 +118,146 @@ func (r *DeviceRepo) FetchByReference(ctx context.Context, kind models.Reference
|
||||
return r.populate(ctx, records)
|
||||
}
|
||||
|
||||
func (r *DeviceRepo) Save(ctx context.Context, device *models.Device, mode models.SaveMode) error {
|
||||
func (r *DeviceRepo) SaveMany(ctx context.Context, mode models.SaveMode, devices []models.Device) error {
|
||||
tx, err := r.DBX.Beginx()
|
||||
if err != nil {
|
||||
return dbErr(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
record := deviceRecord{
|
||||
ID: device.ID,
|
||||
BridgeID: device.BridgeID,
|
||||
InternalID: device.InternalID,
|
||||
Icon: device.Icon,
|
||||
Name: device.Name,
|
||||
Capabilities: strings.Join(models.DeviceCapabilitiesToStrings(device.Capabilities), ","),
|
||||
ButtonNames: strings.Join(device.ButtonNames, ","),
|
||||
}
|
||||
for i, device := range devices {
|
||||
scenesJSON, err := json.Marshal(device.SceneAssignments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if device.ID > 0 {
|
||||
_, err := tx.NamedExecContext(ctx, `
|
||||
record := deviceRecord{
|
||||
ID: device.ID,
|
||||
BridgeID: device.BridgeID,
|
||||
InternalID: device.InternalID,
|
||||
SceneAssignmentJSON: scenesJSON,
|
||||
Icon: device.Icon,
|
||||
Name: device.Name,
|
||||
Capabilities: strings.Join(models.DeviceCapabilitiesToStrings(device.Capabilities), ","),
|
||||
ButtonNames: strings.Join(device.ButtonNames, ","),
|
||||
}
|
||||
|
||||
if device.ID > 0 {
|
||||
_, err := tx.NamedExecContext(ctx, `
|
||||
UPDATE device SET
|
||||
internal_id = :internal_id,
|
||||
icon = :icon,
|
||||
name = :name,
|
||||
capabilities = :capabilities,
|
||||
button_names = :button_names
|
||||
button_names = :button_names,
|
||||
scene_assignments = :scene_assignments
|
||||
WHERE id=:id
|
||||
`, record)
|
||||
if err != nil {
|
||||
return dbErr(err)
|
||||
}
|
||||
|
||||
// Let's just be lazy for now, optimize later if need be.
|
||||
if mode == 0 || mode&models.SMTags != 0 {
|
||||
_, err = tx.ExecContext(ctx, "DELETE FROM device_tag WHERE device_id=?", record.ID)
|
||||
if err != nil {
|
||||
return dbErr(err)
|
||||
}
|
||||
|
||||
// Let's just be lazy for now, optimize later if need be.
|
||||
if mode == 0 || mode&models.SMTags != 0 {
|
||||
_, err = tx.ExecContext(ctx, "DELETE FROM device_tag WHERE device_id=?", record.ID)
|
||||
if err != nil {
|
||||
return dbErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
if mode == 0 || mode&models.SMProperties != 0 {
|
||||
_, err = tx.ExecContext(ctx, "DELETE FROM device_property WHERE device_id=?", record.ID)
|
||||
if err != nil {
|
||||
return dbErr(err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res, err := tx.NamedExecContext(ctx, `
|
||||
INSERT INTO device (bridge_id, internal_id, icon, name, capabilities, button_names)
|
||||
VALUES (:bridge_id, :internal_id, :icon, :name, :capabilities, :button_names)
|
||||
`, record)
|
||||
if err != nil {
|
||||
return dbErr(err)
|
||||
}
|
||||
|
||||
lastID, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
return dbErr(err)
|
||||
}
|
||||
|
||||
record.ID = int(lastID)
|
||||
devices[i].ID = int(lastID)
|
||||
}
|
||||
|
||||
if mode == 0 || mode&models.SMTags != 0 {
|
||||
for _, tag := range device.Tags {
|
||||
_, err := tx.ExecContext(ctx, "INSERT INTO device_tag (device_id, tag_name) VALUES (?, ?)", record.ID, tag)
|
||||
if err != nil {
|
||||
return dbErr(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mode == 0 || mode&models.SMProperties != 0 {
|
||||
_, err = tx.ExecContext(ctx, "DELETE FROM device_property WHERE device_id=?", record.ID)
|
||||
if err != nil {
|
||||
return dbErr(err)
|
||||
for key, value := range device.UserProperties {
|
||||
_, err := tx.ExecContext(ctx, "INSERT INTO device_property (device_id, prop_key, prop_value, is_user) VALUES (?, ?, ?, 1)",
|
||||
record.ID, key, value,
|
||||
)
|
||||
if err != nil {
|
||||
return dbErr(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res, err := tx.NamedExecContext(ctx, `
|
||||
INSERT INTO device (bridge_id, internal_id, icon, name, capabilities, button_names)
|
||||
VALUES (:bridge_id, :internal_id, :icon, :name, :capabilities, :button_names)
|
||||
`, record)
|
||||
if err != nil {
|
||||
return dbErr(err)
|
||||
}
|
||||
|
||||
lastID, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
return dbErr(err)
|
||||
}
|
||||
for key, value := range device.DriverProperties {
|
||||
j, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
// Eh, it'll get filled by the driver anyway
|
||||
continue
|
||||
}
|
||||
|
||||
record.ID = int(lastID)
|
||||
device.ID = int(lastID)
|
||||
}
|
||||
|
||||
if mode == 0 || mode&models.SMTags != 0 {
|
||||
for _, tag := range device.Tags {
|
||||
_, err := tx.ExecContext(ctx, "INSERT INTO device_tag (device_id, tag_name) VALUES (?, ?)", record.ID, tag)
|
||||
if err != nil {
|
||||
return dbErr(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mode == 0 || mode&models.SMProperties != 0 {
|
||||
for key, value := range device.UserProperties {
|
||||
_, err := tx.ExecContext(ctx, "INSERT INTO device_property (device_id, prop_key, prop_value, is_user) VALUES (?, ?, ?, 1)",
|
||||
record.ID, key, value,
|
||||
)
|
||||
if err != nil {
|
||||
return dbErr(err)
|
||||
_, err = tx.ExecContext(ctx, "INSERT INTO device_property (device_id, prop_key, prop_value, is_user) VALUES (?, ?, ?, 0)",
|
||||
record.ID, key, string(j),
|
||||
)
|
||||
if err != nil {
|
||||
// Return err here anyway, it might put the tx in a bad state to ignore it.
|
||||
return dbErr(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for key, value := range device.DriverProperties {
|
||||
j, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
// Eh, it'll get filled by the driver anyway
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, "INSERT INTO device_property (device_id, prop_key, prop_value, is_user) VALUES (?, ?, ?, 0)",
|
||||
record.ID, key, string(j),
|
||||
)
|
||||
if err != nil {
|
||||
// Return err here anyway, it might put the tx in a bad state to ignore it.
|
||||
return dbErr(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mode == 0 || mode&models.SMState != 0 {
|
||||
_, err = tx.NamedExecContext(ctx, `
|
||||
if mode == 0 || mode&models.SMState != 0 {
|
||||
_, err = tx.NamedExecContext(ctx, `
|
||||
REPLACE INTO device_state(device_id, hue, saturation, kelvin, power, intensity)
|
||||
VALUES (:device_id, :hue, :saturation, :kelvin, :power, :intensity)
|
||||
`, deviceStateRecord{
|
||||
DeviceID: record.ID,
|
||||
Hue: device.State.Color.Hue,
|
||||
Saturation: device.State.Color.Saturation,
|
||||
Kelvin: device.State.Color.Kelvin,
|
||||
Power: device.State.Power,
|
||||
Intensity: device.State.Intensity,
|
||||
})
|
||||
if err != nil {
|
||||
return dbErr(err)
|
||||
`, deviceStateRecord{
|
||||
DeviceID: record.ID,
|
||||
Hue: device.State.Color.Hue,
|
||||
Saturation: device.State.Color.Saturation,
|
||||
Kelvin: device.State.Color.Kelvin,
|
||||
Power: device.State.Power,
|
||||
Intensity: device.State.Intensity,
|
||||
})
|
||||
if err != nil {
|
||||
return dbErr(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (r *DeviceRepo) Save(ctx context.Context, device *models.Device, mode models.SaveMode) error {
|
||||
devices := []models.Device{*device}
|
||||
|
||||
err := r.SaveMany(ctx, mode, devices)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*device = devices[0]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DeviceRepo) Delete(ctx context.Context, device *models.Device) error {
|
||||
_, err := r.DBX.ExecContext(ctx, "DELETE FROM device WHERE Id=?", device.ID)
|
||||
if err != nil {
|
||||
@@ -328,12 +350,15 @@ func (r *DeviceRepo) populate(ctx context.Context, records []deviceRecord) ([]mo
|
||||
InternalID: record.InternalID,
|
||||
Icon: record.Icon,
|
||||
Name: record.Name,
|
||||
SceneAssignments: make([]models.DeviceSceneAssignment, 0, 4),
|
||||
ButtonNames: strings.Split(record.ButtonNames, ","),
|
||||
DriverProperties: make(map[string]interface{}, 8),
|
||||
UserProperties: make(map[string]string, 8),
|
||||
Tags: make([]string, 0, 8),
|
||||
}
|
||||
|
||||
_ = json.Unmarshal(record.SceneAssignmentJSON, &device.SceneAssignments)
|
||||
|
||||
if device.ButtonNames[0] == "" {
|
||||
device.ButtonNames = device.ButtonNames[:0]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type sceneRecord struct {
|
||||
ID int `db:"id"`
|
||||
Name string `db:"name"`
|
||||
IntervalMS int64 `db:"interval_ms"`
|
||||
RoleJSON json.RawMessage `db:"roles"`
|
||||
}
|
||||
|
||||
type SceneRepo struct {
|
||||
DBX *sqlx.DB
|
||||
}
|
||||
|
||||
func (r *SceneRepo) Find(ctx context.Context, id int) (*models.Scene, error) {
|
||||
var scene sceneRecord
|
||||
err := r.DBX.GetContext(ctx, &scene, "SELECT * FROM scene WHERE id = ?", id)
|
||||
if err != nil {
|
||||
return nil, dbErr(err)
|
||||
}
|
||||
|
||||
return r.populateOne(&scene)
|
||||
}
|
||||
|
||||
func (r *SceneRepo) FetchAll(ctx context.Context) ([]models.Scene, error) {
|
||||
scenes := make([]sceneRecord, 0, 8)
|
||||
err := r.DBX.SelectContext(ctx, &scenes, "SELECT * FROM scene")
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return []models.Scene{}, nil
|
||||
}
|
||||
|
||||
return nil, dbErr(err)
|
||||
}
|
||||
|
||||
return r.populate(scenes)
|
||||
}
|
||||
|
||||
func (r *SceneRepo) Save(ctx context.Context, scene *models.Scene) error {
|
||||
j, err := json.Marshal(scene.Roles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if scene.ID > 0 {
|
||||
_, err := r.DBX.ExecContext(
|
||||
ctx,
|
||||
"UPDATE scene SET name = ?, interval_ms = ?, roles = ? WHERE id = ?",
|
||||
scene.Name, scene.IntervalMS, j, scene.ID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return dbErr(err)
|
||||
}
|
||||
} else {
|
||||
rs, err := r.DBX.ExecContext(
|
||||
ctx,
|
||||
"INSERT INTO scene (name, interval_ms, roles) VALUES (?, ?, ?)",
|
||||
scene.Name, scene.IntervalMS, j,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return dbErr(err)
|
||||
}
|
||||
|
||||
id, err := rs.LastInsertId()
|
||||
if err != nil {
|
||||
return dbErr(err)
|
||||
}
|
||||
|
||||
scene.ID = int(id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SceneRepo) Delete(ctx context.Context, scene *models.Scene) error {
|
||||
_, err := r.DBX.ExecContext(ctx, "DELETE FROM scene WHERE id = ?", scene.ID)
|
||||
if err != nil {
|
||||
return dbErr(err)
|
||||
}
|
||||
|
||||
scene.ID = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SceneRepo) populateOne(record *sceneRecord) (*models.Scene, error) {
|
||||
records, err := r.populate([]sceneRecord{*record})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &records[0], nil
|
||||
}
|
||||
|
||||
func (r *SceneRepo) populate(records []sceneRecord) ([]models.Scene, error) {
|
||||
res := make([]models.Scene, 0, len(records))
|
||||
|
||||
for _, record := range records {
|
||||
scene := models.Scene{
|
||||
ID: record.ID,
|
||||
Name: record.Name,
|
||||
IntervalMS: record.IntervalMS,
|
||||
Roles: make([]models.SceneRole, 0, 8),
|
||||
}
|
||||
|
||||
err := json.Unmarshal(record.RoleJSON, &scene.Roles)
|
||||
if err != nil {
|
||||
return nil, dbErr(err)
|
||||
}
|
||||
|
||||
res = append(res, scene)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
@@ -26,9 +26,11 @@ func (c *ColorValue) String() string {
|
||||
return fmt.Sprintf("k:%d", c.Kelvin)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("hs:%g,%g", c.Hue, c.Saturation)
|
||||
return fmt.Sprintf("hs:%.4g,%.3g", c.Hue, c.Saturation)
|
||||
}
|
||||
|
||||
|
||||
|
||||
func ParseColorValue(raw string) (ColorValue, error) {
|
||||
tokens := strings.SplitN(raw, ":", 2)
|
||||
if len(tokens) != 2 {
|
||||
|
||||
+83
-11
@@ -2,21 +2,25 @@ package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
ID int `json:"id"`
|
||||
BridgeID int `json:"bridgeID"`
|
||||
InternalID string `json:"internalId"`
|
||||
Icon string `json:"icon"`
|
||||
Name string `json:"name"`
|
||||
Capabilities []DeviceCapability `json:"capabilities"`
|
||||
ButtonNames []string `json:"buttonNames"`
|
||||
DriverProperties map[string]interface{} `json:"driverProperties"`
|
||||
UserProperties map[string]string `json:"userProperties"`
|
||||
State DeviceState `json:"state"`
|
||||
Tags []string `json:"tags"`
|
||||
ID int `json:"id"`
|
||||
BridgeID int `json:"bridgeID"`
|
||||
InternalID string `json:"internalId"`
|
||||
Icon string `json:"icon"`
|
||||
Name string `json:"name"`
|
||||
Capabilities []DeviceCapability `json:"capabilities"`
|
||||
ButtonNames []string `json:"buttonNames"`
|
||||
DriverProperties map[string]interface{} `json:"driverProperties"`
|
||||
UserProperties map[string]string `json:"userProperties"`
|
||||
SceneAssignments []DeviceSceneAssignment `json:"sceneAssignments"`
|
||||
SceneState *DeviceState `json:"sceneState"`
|
||||
State DeviceState `json:"state"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
type DeviceUpdate struct {
|
||||
@@ -37,6 +41,13 @@ type DeviceState struct {
|
||||
Temperature float64 `json:"temperature"`
|
||||
}
|
||||
|
||||
type DeviceScene struct {
|
||||
SceneID int `json:"sceneId"`
|
||||
Time time.Time `json:"time"`
|
||||
DurationMS int64 `json:"duration"`
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
|
||||
type NewDeviceState struct {
|
||||
Power *bool `json:"power"`
|
||||
Color *string `json:"color"`
|
||||
@@ -58,6 +69,7 @@ type DeviceRepository interface {
|
||||
Find(ctx context.Context, id int) (*Device, error)
|
||||
FetchByReference(ctx context.Context, kind ReferenceKind, value string) ([]Device, error)
|
||||
Save(ctx context.Context, device *Device, mode SaveMode) error
|
||||
SaveMany(ctx context.Context, mode SaveMode, devices []Device) error
|
||||
Delete(ctx context.Context, device *Device) error
|
||||
}
|
||||
|
||||
@@ -178,3 +190,63 @@ func (d *Device) SetState(newState NewDeviceState) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *NewDeviceState) RelativeTo(device Device) NewDeviceState {
|
||||
n := NewDeviceState{}
|
||||
|
||||
if s.Intensity != nil {
|
||||
intensity := device.State.Intensity * *s.Intensity
|
||||
n.Intensity = &intensity
|
||||
}
|
||||
if s.Color != nil {
|
||||
c, err := ParseColorValue(*s.Color)
|
||||
if err == nil {
|
||||
c.Hue = math.Mod(device.State.Color.Hue+c.Hue, 360)
|
||||
c.Saturation *= device.State.Color.Saturation
|
||||
c.Kelvin += device.State.Color.Kelvin
|
||||
}
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func (s *NewDeviceState) Interpolate(other NewDeviceState, fac float64) NewDeviceState {
|
||||
n := NewDeviceState{}
|
||||
|
||||
if s.Power != nil && other.Power != nil {
|
||||
if fac >= 0.5 {
|
||||
n.Power = other.Power
|
||||
} else {
|
||||
n.Power = s.Power
|
||||
}
|
||||
}
|
||||
|
||||
if s.Color != nil && other.Color != nil {
|
||||
sc, err := ParseColorValue(*s.Color)
|
||||
oc, err2 := ParseColorValue(*other.Color)
|
||||
if err == nil && err2 == nil {
|
||||
rc := ColorValue{}
|
||||
rc.Hue = interpolateFloat(sc.Hue, oc.Hue, fac)
|
||||
rc.Saturation = interpolateFloat(sc.Saturation, oc.Saturation, fac)
|
||||
rc.Kelvin = interpolateInt(sc.Kelvin, oc.Kelvin, fac)
|
||||
|
||||
rcStr := rc.String()
|
||||
n.Color = &rcStr
|
||||
}
|
||||
}
|
||||
|
||||
if s.Intensity != nil && other.Intensity != nil {
|
||||
n.Intensity = new(float64)
|
||||
*n.Intensity = interpolateFloat(*s.Intensity, *other.Intensity, fac)
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func interpolateFloat(a, b, fac float64) float64 {
|
||||
return (a * (1 - fac)) + (b * fac)
|
||||
}
|
||||
|
||||
func interpolateInt(a, b int, fac float64) int {
|
||||
return int((float64(a) * (1 - fac)) + (float64(b) * fac))
|
||||
}
|
||||
|
||||
@@ -22,3 +22,10 @@ var ErrInvalidPacketSize = errors.New("invalid packet size")
|
||||
var ErrReadTimeout = errors.New("read timeout")
|
||||
var ErrUnrecognizedPacketType = errors.New("packet type not recognized")
|
||||
var ErrBridgeRunningRequired = errors.New("this operation cannot be performed when bridge is not running")
|
||||
|
||||
var ErrSceneInvalidInterval = errors.New("scene interval must be 0 (=disabled) or greater")
|
||||
var ErrSceneNoRoles = errors.New("scene cannot have zero rules")
|
||||
var ErrSceneRoleNoStates = errors.New("scene rule has no states")
|
||||
var ErrSceneRoleUnsupportedOrdering = errors.New("scene rule has an unsupported ordering")
|
||||
var ErrSceneRoleUnknownEffect = errors.New("scene rule has an unknown effect")
|
||||
var ErrSceneRoleUnknownPowerMode = errors.New("scene rule has an unknown power mode")
|
||||
|
||||
+29
-6
@@ -5,6 +5,7 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type EventHandler struct {
|
||||
@@ -168,6 +169,20 @@ func (c *EventCondition) checkDevice(key string, device Device) (matches bool, s
|
||||
}
|
||||
|
||||
return c.matches(strconv.FormatFloat(device.State.Temperature, 'f', -1, 64)), false
|
||||
case "scene":
|
||||
if len(device.SceneAssignments) == 0 {
|
||||
return false, false
|
||||
}
|
||||
|
||||
sceneId := -1
|
||||
for _, assignment := range device.SceneAssignments {
|
||||
duration := time.Duration(assignment.DurationMS) * time.Millisecond
|
||||
if duration <= 0 || time.Now().Before(assignment.StartTime.Add(duration)) {
|
||||
sceneId = assignment.SceneID
|
||||
}
|
||||
}
|
||||
|
||||
return c.matches(strconv.Itoa(sceneId)), false
|
||||
default:
|
||||
return false, true
|
||||
}
|
||||
@@ -214,12 +229,14 @@ func (c *EventCondition) matches(value string) bool {
|
||||
}
|
||||
|
||||
type EventAction struct {
|
||||
SetPower *bool `json:"setPower"`
|
||||
SetColor *string `json:"setColor"`
|
||||
SetIntensity *float64 `json:"setIntensity"`
|
||||
SetTemperature *int `json:"setTemperature"`
|
||||
AddIntensity *float64 `json:"addIntensity"`
|
||||
FireEvent *Event `json:"fireEvent"`
|
||||
SetPower *bool `json:"setPower"`
|
||||
SetColor *string `json:"setColor"`
|
||||
SetIntensity *float64 `json:"setIntensity"`
|
||||
SetTemperature *int `json:"setTemperature"`
|
||||
AddIntensity *float64 `json:"addIntensity"`
|
||||
FireEvent *Event `json:"fireEvent"`
|
||||
SetScene *DeviceSceneAssignment `json:"setScene"`
|
||||
PushScene *DeviceSceneAssignment `json:"pushScene"`
|
||||
}
|
||||
|
||||
func (action *EventAction) Apply(other EventAction) {
|
||||
@@ -238,6 +255,12 @@ func (action *EventAction) Apply(other EventAction) {
|
||||
if action.FireEvent == nil {
|
||||
action.FireEvent = other.FireEvent
|
||||
}
|
||||
if action.SetScene == nil {
|
||||
action.SetScene = other.SetScene
|
||||
}
|
||||
if action.PushScene == nil {
|
||||
action.PushScene = other.PushScene
|
||||
}
|
||||
}
|
||||
|
||||
func (c EventCondition) String() string {
|
||||
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Scene struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
IntervalMS int64 `json:"interval"`
|
||||
Roles []SceneRole `json:"roles"`
|
||||
}
|
||||
|
||||
func (s *Scene) Validate() error {
|
||||
if s.IntervalMS < 0 {
|
||||
return ErrSceneInvalidInterval
|
||||
}
|
||||
if len(s.Roles) == 0 {
|
||||
return ErrSceneNoRoles
|
||||
}
|
||||
|
||||
for _, role := range s.Roles {
|
||||
err := role.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Scene) Role(device *Device) *SceneRole {
|
||||
index := s.RoleIndex(device)
|
||||
if index == -1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &s.Roles[index]
|
||||
}
|
||||
|
||||
func (s *Scene) RoleIndex(device *Device) int {
|
||||
for i, role := range s.Roles {
|
||||
if role.TargetKind.Matches(device, role.TargetValue) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
type SceneEffect string
|
||||
|
||||
const (
|
||||
SEStatic SceneEffect = "Static"
|
||||
SERandom SceneEffect = "Random"
|
||||
SEGradient SceneEffect = "Gradient"
|
||||
SEWalkingGradient SceneEffect = "WalkingGradient"
|
||||
SETransition SceneEffect = "Transition"
|
||||
)
|
||||
|
||||
type SceneRole struct {
|
||||
Effect SceneEffect `json:"effect"`
|
||||
PowerMode ScenePowerMode `json:"powerMode"`
|
||||
TargetKind ReferenceKind `json:"targetKind"`
|
||||
TargetValue string `json:"targetValue"`
|
||||
Interpolate bool `json:"interpolate"`
|
||||
Relative bool `json:"relative"`
|
||||
Order string `json:"order"`
|
||||
States []NewDeviceState `json:"states"`
|
||||
}
|
||||
|
||||
type SceneRunContext struct {
|
||||
Index int
|
||||
Length int
|
||||
IntervalNumber int64
|
||||
IntervalMax int64
|
||||
}
|
||||
|
||||
func (d *SceneRunContext) PositionFacShifted() float64 {
|
||||
shiftFac := float64(d.IntervalNumber%int64(d.Length)) / float64(d.Length)
|
||||
|
||||
return math.Mod(d.PositionFac()+shiftFac, 1)
|
||||
}
|
||||
|
||||
func (d *SceneRunContext) PositionFac() float64 {
|
||||
return float64(d.Index) / float64(d.Length)
|
||||
}
|
||||
|
||||
func (d *SceneRunContext) IntervalFac() float64 {
|
||||
fac := float64(d.IntervalNumber) / float64(d.IntervalMax)
|
||||
if fac > 1 {
|
||||
return 1
|
||||
} else if fac < 0 {
|
||||
return 0
|
||||
} else {
|
||||
return fac
|
||||
}
|
||||
}
|
||||
|
||||
func (r *SceneRole) Validate() error {
|
||||
if len(r.States) == 0 {
|
||||
return ErrSceneRoleNoStates
|
||||
}
|
||||
|
||||
switch r.TargetKind {
|
||||
case RKTag, RKBridgeID, RKDeviceID, RKName, RKAll:
|
||||
default:
|
||||
return ErrBadInput
|
||||
}
|
||||
|
||||
switch r.PowerMode {
|
||||
case SPScene, SPDevice, SPBoth:
|
||||
default:
|
||||
return ErrSceneRoleUnknownPowerMode
|
||||
}
|
||||
|
||||
switch r.Effect {
|
||||
case SEStatic, SERandom, SEGradient, SEWalkingGradient, SETransition:
|
||||
default:
|
||||
return ErrSceneRoleUnknownEffect
|
||||
}
|
||||
|
||||
switch r.Order {
|
||||
case "", "-name", "name", "+name", "-id", "id", "+id":
|
||||
default:
|
||||
return ErrSceneRoleUnsupportedOrdering
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SceneRole) ApplyOrder(devices []Device) {
|
||||
if r.Order == "" {
|
||||
return
|
||||
}
|
||||
|
||||
desc := strings.HasPrefix(r.Order, "-")
|
||||
|
||||
orderKey := r.Order
|
||||
if desc || strings.HasPrefix(r.Order, "+") {
|
||||
orderKey = orderKey[1:]
|
||||
}
|
||||
|
||||
switch orderKey {
|
||||
case "id":
|
||||
sort.Slice(devices, func(i, j int) bool {
|
||||
if desc {
|
||||
return devices[i].ID > devices[j].ID
|
||||
}
|
||||
|
||||
return devices[i].ID < devices[j].ID
|
||||
})
|
||||
case "name":
|
||||
sort.Slice(devices, func(i, j int) bool {
|
||||
if desc {
|
||||
return strings.Compare(devices[i].Name, devices[j].Name) < 0
|
||||
}
|
||||
|
||||
return strings.Compare(devices[i].Name, devices[j].Name) > 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (r *SceneRole) ApplyEffect(device *Device, c SceneRunContext) (newState NewDeviceState) {
|
||||
switch r.Effect {
|
||||
case SEStatic:
|
||||
newState = r.States[0]
|
||||
case SERandom:
|
||||
newState = r.State(rand.Float64())
|
||||
case SEGradient:
|
||||
newState = r.State(c.PositionFac())
|
||||
case SEWalkingGradient:
|
||||
newState = r.State(c.PositionFacShifted())
|
||||
case SETransition:
|
||||
newState = r.State(c.IntervalFac())
|
||||
}
|
||||
|
||||
if r.Relative {
|
||||
newState = newState.RelativeTo(*device)
|
||||
}
|
||||
|
||||
switch r.PowerMode {
|
||||
case SPDevice:
|
||||
newState.Power = nil
|
||||
case SPScene:
|
||||
// Do nothing
|
||||
case SPBoth:
|
||||
if newState.Power != nil {
|
||||
powerIntersection := *newState.Power && device.State.Power
|
||||
newState.Power = &powerIntersection
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *SceneRole) State(fac float64) NewDeviceState {
|
||||
if len(r.States) == 0 {
|
||||
return NewDeviceState{}
|
||||
} else if len(r.States) == 1 {
|
||||
return r.States[0]
|
||||
}
|
||||
|
||||
if r.Interpolate {
|
||||
if len(r.States) == 2 {
|
||||
return r.States[0].Interpolate(r.States[1], fac)
|
||||
} else {
|
||||
pos := fac * float64(len(r.States)-1)
|
||||
start := math.Floor(pos)
|
||||
localFac := pos - start
|
||||
|
||||
return r.States[int(start)].Interpolate(r.States[int(start)+1], localFac)
|
||||
}
|
||||
} else {
|
||||
index := int(fac * float64(len(r.States)))
|
||||
if index == len(r.States) {
|
||||
index = len(r.States) - 1
|
||||
}
|
||||
|
||||
return r.States[index]
|
||||
}
|
||||
}
|
||||
|
||||
type ScenePowerMode string
|
||||
|
||||
const (
|
||||
SPDevice ScenePowerMode = "Device" // Device state decides power. Scene state may only power off.
|
||||
SPScene ScenePowerMode = "Scene" // Scene state decide power.
|
||||
SPBoth ScenePowerMode = "Both" // Power is only on if both Device and Scene says it is.
|
||||
)
|
||||
|
||||
// DeviceSceneAssignment is an entry on the device stack. StartTime and DurationMS are only respected when the scene
|
||||
// instance is created for the group.
|
||||
type DeviceSceneAssignment struct {
|
||||
SceneID int `json:"sceneId"`
|
||||
Group string `json:"group"`
|
||||
StartTime time.Time `json:"start"`
|
||||
DurationMS int64 `json:"durationMs"`
|
||||
}
|
||||
|
||||
type SceneRepository interface {
|
||||
Find(ctx context.Context, id int) (*Scene, error)
|
||||
FetchAll(ctx context.Context) ([]Scene, error)
|
||||
Save(ctx context.Context, bridge *Scene) error
|
||||
Delete(ctx context.Context, bridge *Scene) error
|
||||
}
|
||||
+51
-1
@@ -15,6 +15,56 @@ var (
|
||||
RKName ReferenceKind = "Name"
|
||||
)
|
||||
|
||||
func (rk ReferenceKind) Matches(device *Device, value string) bool {
|
||||
switch rk {
|
||||
case RKName:
|
||||
if strings.HasPrefix(value, "*") {
|
||||
return strings.HasSuffix(device.Name, value[1:])
|
||||
} else if strings.HasSuffix(value, "*") {
|
||||
return strings.HasPrefix(device.Name, value[:len(value)-1])
|
||||
} else {
|
||||
return device.Name == value
|
||||
}
|
||||
case RKDeviceID:
|
||||
idStr := strconv.Itoa(device.ID)
|
||||
for _, idStr2 := range strings.Split(value, ",") {
|
||||
if idStr == idStr2 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
case RKBridgeID:
|
||||
idStr := strconv.Itoa(device.BridgeID)
|
||||
for _, idStr2 := range strings.Split(value, ",") {
|
||||
if idStr == idStr2 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
case RKTag:
|
||||
hadAny := false
|
||||
for _, tag := range strings.Split(value, ",") {
|
||||
if strings.HasPrefix(tag, "-") {
|
||||
if device.HasTag(tag[1:]) {
|
||||
return false
|
||||
}
|
||||
} else if strings.HasPrefix(tag, "+") {
|
||||
if !device.HasTag(tag[1:]) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if device.HasTag(tag) {
|
||||
hadAny = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hadAny
|
||||
case RKAll:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func ParseFetchString(fetchStr string) (ReferenceKind, string) {
|
||||
if strings.HasPrefix(fetchStr, "tag:") {
|
||||
return RKTag, fetchStr[4:]
|
||||
@@ -34,4 +84,4 @@ func ParseFetchString(fetchStr string) (ReferenceKind, string) {
|
||||
|
||||
return RKDeviceID, fetchStr
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
name: Evening
|
||||
interval: 1500
|
||||
roles:
|
||||
- effect: WalkingGradient
|
||||
power_mode: Device
|
||||
target_kind: Tag
|
||||
target_value: Hexagon
|
||||
interpolate: true
|
||||
relative: false
|
||||
order: -name
|
||||
states:
|
||||
- color: 'hs:30,1'
|
||||
intensity: 0.20
|
||||
- color: 'hs:30,1'
|
||||
intensity: 0.15
|
||||
|
||||
- effect: Gradient
|
||||
power_mode: Device
|
||||
target_kind: Tag
|
||||
target_value: SquareLeft
|
||||
interpolate: true
|
||||
relative: false
|
||||
order: +name
|
||||
states:
|
||||
- color: 'hs:25,1'
|
||||
intensity: 0.075
|
||||
- color: 'hs:50,1'
|
||||
intensity: 0.15
|
||||
|
||||
- effect: Random
|
||||
power_mode: Device
|
||||
target_kind: Tag
|
||||
target_value: SquareRight
|
||||
interpolate: true
|
||||
relative: false
|
||||
order: +name
|
||||
states:
|
||||
- color: 'hs:220,0.7'
|
||||
intensity: 0.22
|
||||
- color: 'hs:220,0.6'
|
||||
intensity: 0.25
|
||||
|
||||
- effect: Static
|
||||
power_mode: Device
|
||||
target_kind: Tag
|
||||
target_value: Accent
|
||||
interpolate: true
|
||||
relative: false
|
||||
order: +name
|
||||
states:
|
||||
- color: 'hs:250,0.7'
|
||||
intensity: 0.25
|
||||
|
||||
- effect: Static
|
||||
power_mode: Device
|
||||
target_kind: All
|
||||
target_value: ""
|
||||
interpolate: true
|
||||
relative: false
|
||||
order: +name
|
||||
states:
|
||||
- color: 'k:2000'
|
||||
intensity: 0.3
|
||||
@@ -0,0 +1,26 @@
|
||||
name: Flash
|
||||
interval: 0
|
||||
roles:
|
||||
- effect: Static
|
||||
power_mode: Device
|
||||
target_kind: Tag
|
||||
target_value: Nanoleaf
|
||||
states:
|
||||
- intensity: 1
|
||||
color: hs:220,0.1
|
||||
|
||||
- effect: Static
|
||||
power_mode: Device
|
||||
target_kind: Tag
|
||||
target_value: Hue
|
||||
states:
|
||||
- intensity: 1
|
||||
color: k:6500
|
||||
|
||||
- effect: Static
|
||||
power_mode: Device
|
||||
target_kind: All
|
||||
target_value: ""
|
||||
states:
|
||||
- intensity: 1
|
||||
color: hs:0,0
|
||||
@@ -0,0 +1,57 @@
|
||||
name: Late
|
||||
interval: 60000
|
||||
roles:
|
||||
- effect: Random
|
||||
power_mode: Device
|
||||
target_kind: Tag
|
||||
target_value: Hexagon
|
||||
interpolate: true
|
||||
relative: false
|
||||
order: +name
|
||||
states:
|
||||
- color: 'hs:35,1'
|
||||
intensity: 0.08
|
||||
- color: 'hs:35,1'
|
||||
intensity: 0.085
|
||||
- color: 'hs:35,1'
|
||||
intensity: 0.09
|
||||
- color: 'hs:35,1'
|
||||
intensity: 0.12
|
||||
|
||||
- effect: Gradient
|
||||
power_mode: Both
|
||||
target_kind: Tag
|
||||
target_value: Square
|
||||
interpolate: true
|
||||
relative: false
|
||||
order: +name
|
||||
states:
|
||||
- color: 'hs:25,1'
|
||||
intensity: 0.05
|
||||
power: off
|
||||
- color: 'hs:30,1'
|
||||
intensity: 0.06
|
||||
power: off
|
||||
- color: 'hs:35,1'
|
||||
intensity: 0.07
|
||||
power: on
|
||||
|
||||
- effect: Static
|
||||
power_mode: Device
|
||||
target_kind: Tag
|
||||
target_value: Nightstand
|
||||
interpolate: true
|
||||
relative: false
|
||||
order: +name
|
||||
states:
|
||||
- color: 'k:2000'
|
||||
intensity: 0.15
|
||||
|
||||
- effect: Static
|
||||
power_mode: Scene
|
||||
target_kind: All
|
||||
target_value: ''
|
||||
interpolate: true
|
||||
relative: false
|
||||
states:
|
||||
- power: false
|
||||
@@ -0,0 +1,40 @@
|
||||
name: Morning
|
||||
interval: 1000
|
||||
roles:
|
||||
- effect: WalkingGradient
|
||||
power_mode: Device
|
||||
target_kind: Tag
|
||||
target_value: Hexagon
|
||||
interpolate: true
|
||||
relative: false
|
||||
order: +name
|
||||
states:
|
||||
- color: 'hs:220,0.4'
|
||||
intensity: 0.65
|
||||
- color: 'hs:220,0.5'
|
||||
intensity: 0.50
|
||||
|
||||
- effect: Random
|
||||
power_mode: Device
|
||||
target_kind: Tag
|
||||
target_value: Square
|
||||
interpolate: true
|
||||
relative: false
|
||||
order: +name
|
||||
states:
|
||||
- color: 'hs:220,0.6'
|
||||
intensity: 0.30
|
||||
- color: 'hs:220,0.5'
|
||||
intensity: 0.35
|
||||
|
||||
- effect: Static
|
||||
power_mode: Device
|
||||
target_kind: All
|
||||
target_value: ''
|
||||
interpolate: true
|
||||
relative: false
|
||||
order: +name
|
||||
states:
|
||||
- color: 'hs:250,0.6'
|
||||
intensity: 0.3
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
name: Saturday
|
||||
interval: 700
|
||||
roles:
|
||||
- effect: WalkingGradient
|
||||
power_mode: Device
|
||||
target_kind: Tag
|
||||
target_value: Hexagon
|
||||
interpolate: true
|
||||
relative: false
|
||||
order: +name
|
||||
states:
|
||||
- color: 'hs:220,0.5'
|
||||
intensity: 0.25
|
||||
- color: 'hs:220,0.5'
|
||||
intensity: 0.30
|
||||
|
||||
- effect: Random
|
||||
power_mode: Device
|
||||
target_kind: Tag
|
||||
target_value: Square
|
||||
interpolate: true
|
||||
relative: false
|
||||
order: +name
|
||||
states:
|
||||
- color: 'hs:220,0.6'
|
||||
intensity: 0.15
|
||||
- color: 'hs:220,0.55'
|
||||
intensity: 0.16
|
||||
- color: 'hs:220,0.5'
|
||||
intensity: 0.20
|
||||
|
||||
- effect: Static
|
||||
power_mode: Device
|
||||
target_kind: All
|
||||
target_value: ''
|
||||
interpolate: true
|
||||
relative: false
|
||||
order: +name
|
||||
states:
|
||||
- color: 'hs:250,0.7'
|
||||
intensity: 0.3
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE scene
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
interval_ms INT NOT NULL,
|
||||
roles JSON NOT NULL,
|
||||
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE scene;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,9 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE device ADD COLUMN scene_assignments JSON NOT NULL DEFAULT ('[]');
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE device DROP COLUMN IF EXISTS scene_assignments;
|
||||
-- +goose StatementEnd
|
||||
Reference in New Issue
Block a user