Browse Source

this does not work, it needs massive changes.

pull/1/head
Gisle Aune 3 years ago
parent
commit
f87c69efc7
  1. 62
      app/api/devices.go
  2. 37
      app/api/scenes.go
  3. 5
      app/config/db.go
  4. 4
      app/config/repo.go
  5. 5
      app/server.go
  6. 25
      app/services/events.go
  7. 5
      app/services/publish.go
  8. 212
      app/services/scene/manager.go
  9. 132
      app/services/scene/scene.go
  10. 186
      internal/mysql/devicerepo.go
  11. 118
      internal/mysql/scenerepo.go
  12. 24
      models/device.go
  13. 65
      models/scene.go
  14. 17
      scripts/20210926135923_scene.sql
  15. 9
      scripts/20210926152011_device_sceneassignment.sql

62
app/api/devices.go

@ -3,11 +3,14 @@ package api
import (
"context"
"git.aiterp.net/lucifer/new-server/app/config"
"git.aiterp.net/lucifer/new-server/app/services/scene"
"git.aiterp.net/lucifer/new-server/models"
"github.com/gin-gonic/gin"
"golang.org/x/sync/errgroup"
"log"
"strconv"
"strings"
"time"
)
func fetchDevices(ctx context.Context, fetchStr string) ([]models.Device, error) {
@ -71,12 +74,15 @@ func Devices(r gin.IRoutes) {
return nil, err
}
_ = scene.GlobalManager().UpdateDevice(ctxOf(c), &devices[i], nil)
set[devices[i].ID] = true
changed = append(changed, devices[i])
}
}
config.PublishChannel <- changed
config.PublishChannel <- scene.GlobalManager().FilterUnassigned(changed)
go func() {
for _, device := range changed {
@ -139,9 +145,11 @@ func Devices(r gin.IRoutes) {
if err != nil {
return nil, err
}
_ = scene.GlobalManager().UpdateDevice(ctxOf(c), &devices[i], nil)
}
config.PublishChannel <- devices
config.PublishChannel <- scene.GlobalManager().FilterUnassigned(devices)
go func() {
for _, device := range devices {
@ -212,4 +220,52 @@ func Devices(r gin.IRoutes) {
return 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
}
assignedScene, 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()
for i := range devices {
devices[i].SceneAssignments = []models.DeviceSceneAssignment{body}
err := scene.GlobalManager().UpdateDevice(ctxOf(c), &devices[i], assignedScene)
if err != nil {
return nil, err
}
}
eg := errgroup.Group{}
for i := range devices {
eg.Go(func() error {
return config.DeviceRepository().Save(ctxOf(c), &devices[i], 0)
})
}
err = eg.Wait()
if err != nil {
return nil, err
}
return devices, nil
}))
}

37
app/api/scenes.go

@ -0,0 +1,37 @@
package api
import (
"git.aiterp.net/lucifer/new-server/app/config"
"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
}
return body, nil
}))
}

5
app/config/db.go

@ -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

4
app/config/repo.go

@ -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()}
}

5
app/server.go

@ -1,10 +1,12 @@
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/scene"
"github.com/gin-gonic/gin"
"log"
)
@ -15,6 +17,8 @@ func StartServer() {
services.ConnectToBridges()
services.CheckNewDevices()
go scene.GlobalManager().Run(context.Background())
gin.SetMode(gin.ReleaseMode)
ginny := gin.New()
@ -25,6 +29,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())))
}

25
app/services/events.go

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"git.aiterp.net/lucifer/new-server/app/config"
"git.aiterp.net/lucifer/new-server/app/services/scene"
"git.aiterp.net/lucifer/new-server/models"
"log"
"math"
@ -150,21 +151,31 @@ func handleEvent(event models.Event) (responses []models.Event) {
}
}
config.PublishChannel <- allDevices
config.PublishChannel <- scene.GlobalManager().FilterUnassigned(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 _, device := range allDevices {
go func(device models.Device) {
err := scene.GlobalManager().UpdateDevice(context.Background(), &device, nil)
if err != nil {
log.Println("Failed to update devices' scene in resposne to event:", err)
}
}(device)
}
}
for _, handler := range deadHandlers {
wg.Add(1)

5
app/services/publish.go

@ -47,6 +47,11 @@ func StartPublisher() {
}
if bridge.ID == 0 {
log.Println("Unknown bridge")
return
}
if bridge.Driver == models.DTLIFX {
return
}
bridge, err := config.BridgeRepository().Find(ctx, devices[0].BridgeID)

212
app/services/scene/manager.go

@ -1,27 +1,84 @@
package scene
import (
"context"
"git.aiterp.net/lucifer/new-server/app/config"
"git.aiterp.net/lucifer/new-server/models"
"log"
"sync"
"time"
)
type Manager struct {
mu sync.Mutex
assignments map[int]*Scene
allocations map[int]*Scene
devices map[int]*models.Device
scenes []*Scene
notifyCh chan struct{}
}
func (mgr *Manager) UpdateDevice(device *models.Device) {
func (mgr *Manager) UpdateDevice(ctx context.Context, device *models.Device, data *models.Scene) error {
mgr.mu.Lock()
defer mgr.mu.Unlock()
scene, err := mgr.findScene(ctx, device.SceneAssignments, data)
if err != nil {
return err
}
if mgr.allocations[device.ID] != nil && mgr.allocations[device.ID] != scene {
mgr.allocations[device.ID].RemoveDevice(*device)
}
if mgr.allocations[device.ID] != scene {
if mgr.allocations == nil {
mgr.allocations = make(map[int]*Scene, 8)
}
mgr.allocations[device.ID] = scene
if scene != nil {
scene.due = true
}
}
if scene != nil {
scene.UpsertDevice(*device)
if mgr.devices == nil {
mgr.devices = make(map[int]*models.Device)
}
mgr.devices[device.ID] = device
} else {
config.PublishChannel <- []models.Device{*device}
}
if mgr.notifyCh != nil {
close(mgr.notifyCh)
mgr.notifyCh = nil
}
return nil
}
func (mgr *Manager) GetUnassigned(devices []models.Device) []models.Device {
func (mgr *Manager) UpdateScene(data *models.Scene) {
mgr.mu.Lock()
for _, scene := range mgr.scenes {
if scene.data.ID == data.ID {
scene.UpdateScene(scene.data)
}
}
mgr.mu.Unlock()
}
func (mgr *Manager) FilterUnassigned(devices []models.Device) []models.Device {
mgr.mu.Lock()
defer mgr.mu.Unlock()
res := make([]models.Device, 0, len(devices))
for _, device := range devices {
if _, ok := mgr.assignments[device.ID]; !ok {
if mgr.allocations[device.ID] == nil {
res = append(res, device)
}
}
@ -29,8 +86,153 @@ func (mgr *Manager) GetUnassigned(devices []models.Device) []models.Device {
return res
}
func (mgr *Manager) Run(ctx context.Context) {
devices, err := config.DeviceRepository().FetchByReference(ctx, models.RKAll, "")
if err == nil {
for _, device := range devices {
_ = mgr.UpdateDevice(ctx, &device, nil)
}
}
ticker := time.NewTicker(time.Millisecond * 101)
defer ticker.Stop()
mgr.mu.Lock()
mgr.notifyCh = make(chan struct{})
mgr.mu.Unlock()
for {
mgr.mu.Lock()
if mgr.notifyCh == nil {
mgr.notifyCh = make(chan struct{})
}
notifyCh := mgr.notifyCh
mgr.mu.Unlock()
select {
case <-ticker.C:
case <-notifyCh:
case <-ctx.Done():
return
}
timeout, cancel := context.WithTimeout(ctx, time.Millisecond*100)
var updates []models.Device
mgr.mu.Lock()
for _, device := range mgr.devices {
if device == nil {
continue
}
if mgr.reScene(timeout, device) {
updates = append(updates, *device)
}
}
for _, scene := range mgr.scenes {
if scene.Due() {
scene.Run()
}
updates = append(updates, scene.Flush()...)
}
mgr.mu.Unlock()
cancel()
if len(updates) > 0 {
log.Println("Updating", len(updates), "devices")
config.PublishChannel <- updates
}
}
}
func (mgr *Manager) reScene(ctx context.Context, device *models.Device) bool {
scene, err := mgr.findScene(ctx, device.SceneAssignments, nil)
if err != nil {
return false
}
if mgr.allocations[device.ID] != nil && mgr.allocations[device.ID] != scene {
mgr.allocations[device.ID].RemoveDevice(*device)
}
if mgr.allocations[device.ID] != scene {
if mgr.allocations == nil {
mgr.allocations = make(map[int]*Scene, 8)
}
mgr.allocations[device.ID] = scene
if scene == nil {
return true
}
mgr.allocations[device.ID].UpsertDevice(*device)
}
return false
}
func (mgr *Manager) findScene(ctx context.Context, assignments []models.DeviceSceneAssignment, data *models.Scene) (*Scene, error) {
if len(assignments) == 0 {
return nil, nil
}
var selected *models.DeviceSceneAssignment
for _, assignment := range assignments {
pos := int64(time.Since(assignment.StartTime) / time.Millisecond)
if assignment.DurationMS == 0 || (pos >= -1 && pos < assignment.DurationMS) {
selected = &assignment
}
}
if selected == nil {
return nil, nil
}
for _, scene := range mgr.scenes {
if scene.group == selected.Group && scene.startTime.Equal(selected.StartTime) && selected.DurationMS == scene.duration.Milliseconds() {
return scene, nil
}
}
if data == nil {
for i := range mgr.scenes {
if mgr.scenes[i].data.ID == selected.SceneID {
data = &mgr.scenes[i].data
break
}
}
}
if data == nil {
var err error
data, err = config.SceneRepository().Find(ctx, selected.SceneID)
if err != nil {
return nil, err
}
}
scene := &Scene{
startTime: selected.StartTime,
duration: time.Duration(selected.DurationMS) * time.Millisecond,
lastRun: 0,
due: true,
group: selected.Group,
data: *data,
roleList: make([][]models.Device, len(data.Roles)),
deviceMap: make(map[int]*models.Device, 8),
roleMap: make(map[int]int, 8),
changes: make(map[int]models.NewDeviceState, 8),
}
mgr.scenes = append(mgr.scenes, scene)
return scene, nil
}
var globalManager = &Manager{}
func Get() *Manager {
func GlobalManager() *Manager {
return globalManager
}

132
app/services/scene/scene.go

@ -1,14 +1,19 @@
package scene
import (
"context"
"git.aiterp.net/lucifer/new-server/app/config"
"git.aiterp.net/lucifer/new-server/models"
"math"
"time"
)
type Scene struct {
startTime time.Time
duration time.Duration
tag string
lastRun int64
due bool
group string
data models.Scene
roleList [][]models.Device
deviceMap map[int]*models.Device
@ -21,32 +26,110 @@ func (s *Scene) UpdateScene(scene models.Scene) {
s.roleMap = make(map[int]int)
for _, device := range s.deviceMap {
s.moveDevice(*device)
s.moveDevice(*device, false)
}
for i, role := range s.data.Roles {
role.ApplyOrder(s.roleList[i])
}
s.Run()
}
func (s *Scene) UpsertDevice(device models.Device) {
s.deviceMap[device.ID] = &device
s.moveDevice(device)
s.moveDevice(device, true)
}
func (s *Scene) RemoveDevice(device models.Device) {
roleIndex, hasRole := s.roleMap[device.ID]
delete(s.deviceMap, device.ID)
delete(s.roleMap, device.ID)
if hasRole {
s.runRole(roleIndex)
}
}
func (s *Scene) Due() bool {
return s.due || s.intervalNumber() != s.lastRun
}
func (s *Scene) Flush() []models.Device {
res := make([]models.Device, 0, 8)
for key, change := range s.changes {
if s.deviceMap[key] != nil {
device := *s.deviceMap[key]
err := device.SetState(change)
if err != nil {
continue
}
res = append(res, device)
}
delete(s.changes, key)
}
return res
}
func (s *Scene) Run() {
s.lastRun = s.intervalNumber()
s.due = false
for index := range s.roleList {
s.runRole(index)
}
}
func (s *Scene) runRole(index int) {
role := s.data.Roles[index]
devices := s.roleList[index]
intervalNumber := s.intervalNumber()
intervalMax := s.intervalMax()
for i, device := range devices {
newState, changed := role.ApplyEffect(&device, models.SceneRunContext{
Index: i,
Length: len(devices),
IntervalNumber: intervalNumber,
IntervalMax: intervalMax,
})
if !changed {
go func() {
_ = config.DeviceRepository().Save(context.Background(), &device, models.SMState)
}()
}
s.changes[device.ID] = newState
}
}
func (s *Scene) moveDevice(device models.Device, run bool) {
oldRole, hasOldRole := s.roleMap[device.ID]
newRole := s.data.RoleIndex(&device)
// If it doesn't move between roles, just update and reorder it.
if hasOldRole && oldRole == newRole {
for i, device2 := range s.roleList[newRole] {
if device2.ID == device.ID {
s.roleList[newRole][i] = device
}
}
if run {
s.data.Roles[newRole].ApplyOrder(s.roleList[newRole])
s.runRole(newRole)
}
return
}
// Take it out of the old role.
if hasOldRole {
for i, device2 := range s.roleList[oldRole] {
if device2.ID == device.ID {
@ -55,33 +138,38 @@ func (s *Scene) moveDevice(device models.Device, run bool) {
}
}
s.runRole(oldRole)
if run {
s.runRole(oldRole)
}
}
s.roleMap[device.ID] = newRole
s.runRole(newRole)
if newRole != -1 {
s.roleMap[device.ID] = newRole
s.roleList[newRole] = append(s.roleList[newRole], device)
if run {
s.data.Roles[newRole].ApplyOrder(s.roleList[newRole])
s.runRole(newRole)
}
}
s.due = true
}
func (s *Scene) intervalNumber() int64 {
intervalDuration := time.Duration(s.data.IntervalMS) * time.Millisecond
if intervalDuration == 0 {
return 0
}
return int64(time.Since(s.startTime) / intervalDuration)
return int64(math.Round(float64(time.Since(s.startTime)) / float64(intervalDuration)))
}
func (s *Scene) intervalFac() float64 {
return clamp(float64(time.Since(s.startTime)) / float64(s.duration))
}
func (s *Scene) intervalMax() int64 {
intervalDuration := time.Duration(s.data.IntervalMS) * time.Millisecond
if intervalDuration == 0 {
return 1
}
func (s *Scene) positionFac(i, len int) float64 {
return clamp(float64(i) / float64(len))
return int64(s.duration / intervalDuration)
}
func clamp(fac float64) float64 {
if fac > 1.00 {
return 1.00
} else if fac < 0.00 {
return 0.00
} else {
return fac
}
}

186
internal/mysql/devicerepo.go

@ -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,25 +118,32 @@ 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,
@ -144,98 +152,111 @@ func (r *DeviceRepo) Save(ctx context.Context, device *models.Device, mode model
button_names = :button_names
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)
}
}
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)
// 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)
}
}
}
} else {
res, err := tx.NamedExecContext(ctx, `
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)
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,
)
lastID, err := res.LastInsertId()
if err != nil {
return dbErr(err)
}
record.ID = int(lastID)
devices[i].ID = int(lastID)
}
for key, value := range device.DriverProperties {
j, err := json.Marshal(value)
if err != nil {
// Eh, it'll get filled by the driver anyway
continue
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)
}
}
}
_, 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.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)
}
}
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 +349,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]
}

118
internal/mysql/scenerepo.go

@ -0,0 +1,118 @@
package mysql
import (
"context"
"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 {
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
}

24
models/device.go

@ -7,17 +7,18 @@ import (
)
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"`
State DeviceState `json:"state"`
Tags []string `json:"tags"`
}
type DeviceUpdate struct {
@ -66,6 +67,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
}

65
models/scene.go

@ -1,10 +1,12 @@
package models
import (
"context"
"math"
"math/rand"
"sort"
"strings"
"time"
)
type Scene struct {
@ -54,15 +56,16 @@ func (s *Scene) RoleIndex(device *Device) int {
type SceneEffect string
const (
SEStatic SceneEffect = "Plain"
SERandom SceneEffect = "Random"
SEGradient SceneEffect = "Gradient"
SETransition SceneEffect = "Transition"
SEStatic SceneEffect = "Plain"
SERandom SceneEffect = "Random"
SEGradient SceneEffect = "Gradient"
SEWalkingGradient SceneEffect = "WalkingGradient"
SETransition SceneEffect = "Transition"
)
type SceneRole struct {
Effect SceneEffect `json:"effect"`
PowerMode ScenePowerMode `json:"overridePower"`
PowerMode ScenePowerMode `json:"powerMode"`
TargetKind ReferenceKind `json:"targetKind"`
TargetValue string `json:"targetValue"`
Interpolate bool `json:"interpolate"`
@ -70,6 +73,34 @@ type SceneRole struct {
States []NewDeviceState `json:"states"`
}
type SceneRunContext struct {
Index int
Length int
IntervalNumber int64
IntervalMax int64
}
func (d *SceneRunContext) PositionFacShifted() float64 {
shiftFac := float64((int64(d.Index)+d.IntervalNumber)%int64(d.Length)) * (1 / 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
@ -134,16 +165,18 @@ func (r *SceneRole) ApplyOrder(devices []Device) {
}
}
func (r *SceneRole) ApplyEffect(device *Device, intervalFac float64, positionFac float64) (newState NewDeviceState, deviceChanged bool) {
func (r *SceneRole) ApplyEffect(device *Device, c SceneRunContext) (newState NewDeviceState, deviceChanged bool) {
switch r.Effect {
case SEStatic:
newState = r.States[0]
case SERandom:
newState = r.State(rand.Float64())
case SEGradient:
newState = r.State(positionFac)
newState = r.State(c.PositionFac())
case SEWalkingGradient:
newState = r.State(c.PositionFacShifted())
case SETransition:
newState = r.State(intervalFac)
newState = r.State(c.IntervalFac())
}
switch r.PowerMode {
@ -196,9 +229,17 @@ const (
SPOverride ScenePowerMode = "Override" // Same as above, but Scene state set Device state' power. This is good for "wake up" scenes.
)
type SceneAssignment struct {
Scene *Scene
Device *Device
Tag string
// DeviceSceneAssignment is an entry on the device stack.
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
}

17
scripts/20210926135923_scene.sql

@ -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

9
scripts/20210926152011_device_sceneassignment.sql

@ -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
Loading…
Cancel
Save