Browse Source

more scene stuff. There's still no command and API to push scenes and unassign (aside from setting a temporary scene and waiting it out).

pull/1/head
Gisle Aune 3 years ago
parent
commit
1b7e495a59
  1. 31
      app/api/scenes.go
  2. 10
      app/client/client.go
  3. 16
      app/client/scene.go
  4. 28
      app/services/publisher/publisher.go
  5. 11
      app/services/publisher/scene.go
  6. 3
      cmd/lucy/main.go
  7. 151
      cmd/lucy/scenecmd.go
  8. 2
      internal/drivers/nanoleaf/bridge.go
  9. 5
      internal/mysql/scenerepo.go
  10. 6
      models/scene.go
  11. 63
      scene-examples/evening.yaml
  12. 9
      scene-examples/flash.yaml
  13. 48
      scene-examples/late.yaml
  14. 40
      scene-examples/morning.yaml

31
app/api/scenes.go

@ -2,6 +2,7 @@ package api
import ( import (
"git.aiterp.net/lucifer/new-server/app/config" "git.aiterp.net/lucifer/new-server/app/config"
"git.aiterp.net/lucifer/new-server/app/services/publisher"
"git.aiterp.net/lucifer/new-server/models" "git.aiterp.net/lucifer/new-server/models"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -32,6 +33,36 @@ func Scenes(r gin.IRoutes) {
return nil, err 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 return body, nil
})) }))
} }

10
app/client/client.go

@ -60,6 +60,16 @@ func (client *Client) PutDeviceTags(ctx context.Context, fetchStr string, addTag
return devices, nil return devices, nil
} }
func (client *Client) AssignDevice(ctx context.Context, fetchStr string, assignment models.DeviceSceneAssignment) ([]models.Device, error) {
devices := make([]models.Device, 0, 16)
err := client.Fetch(ctx, "PUT", "/api/devices/"+fetchStr+"/scene", &devices, assignment)
if err != nil {
return nil, err
}
return devices, nil
}
func (client *Client) FireEvent(ctx context.Context, event models.Event) error { func (client *Client) FireEvent(ctx context.Context, event models.Event) error {
err := client.Fetch(ctx, "POST", "/api/events", nil, event) err := client.Fetch(ctx, "POST", "/api/events", nil, event)
if err != nil { if err != nil {

16
app/client/scene.go

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

28
app/services/publisher/publisher.go

@ -19,6 +19,18 @@ type Publisher struct {
waiting map[int]chan struct{} waiting map[int]chan struct{}
} }
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 { func (p *Publisher) ReloadScenes(ctx context.Context) error {
scenes, err := config.SceneRepository().FetchAll(ctx) scenes, err := config.SceneRepository().FetchAll(ctx)
if err != nil { if err != nil {
@ -26,8 +38,12 @@ func (p *Publisher) ReloadScenes(ctx context.Context) error {
} }
p.mu.Lock() p.mu.Lock()
for _, scene := range scenes {
p.sceneData[scene.ID] = &scene
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() p.mu.Unlock()
@ -90,10 +106,12 @@ func (p *Publisher) Run() {
p.mu.Lock() p.mu.Lock()
for i, scene := range p.scenes { for i, scene := range p.scenes {
if !scene.endTime.IsZero() && time.Now().After(scene.endTime) {
if (!scene.endTime.IsZero() && time.Now().After(scene.endTime)) || scene.Empty() {
deleteList = append(deleteList, i-len(deleteList)) deleteList = append(deleteList, i-len(deleteList))
updatedList = append(updatedList, scene.AllDevices()...) updatedList = append(updatedList, scene.AllDevices()...)
log.Printf("Removing scene instance for %s (%d)", scene.data.Name, scene.data.ID)
for _, device := range scene.AllDevices() { for _, device := range scene.AllDevices() {
p.sceneAssignment[device.ID] = nil p.sceneAssignment[device.ID] = nil
} }
@ -258,6 +276,10 @@ var publisher = Publisher{
waiting: make(map[int]chan struct{}), waiting: make(map[int]chan struct{}),
} }
func Global() *Publisher {
return &publisher
}
func Initialize(ctx context.Context) error { func Initialize(ctx context.Context) error {
err :=publisher.ReloadScenes(ctx) err :=publisher.ReloadScenes(ctx)
if err != nil { if err != nil {

11
app/services/publisher/scene.go

@ -86,6 +86,17 @@ func (s *Scene) RemoveDevice(device models.Device) {
delete(s.roleMap, device.ID) delete(s.roleMap, 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 { func (s *Scene) Due() bool {
if s.due { if s.due {
return true return true

3
cmd/lucy/main.go

@ -96,6 +96,9 @@ func main() {
case "handler": case "handler":
handlerCmd(ctx, c) handlerCmd(ctx, c)
case "scene":
sceneCmd(ctx, c)
// EVENT // EVENT
case "run": case "run":
{ {

151
cmd/lucy/scenecmd.go

@ -0,0 +1,151 @@
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 "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, 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)
}
}
}
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
}

2
internal/drivers/nanoleaf/bridge.go

@ -177,7 +177,7 @@ func (b *bridge) Update(devices []models.Device) {
red, green, blue := color.RGB255() red, green, blue := color.RGB255()
newColor := [4]byte{red, green, blue, 255} newColor := [4]byte{red, green, blue, 255}
if newColor != panel.ColorRGBA { if newColor != panel.ColorRGBA {
panel.update(newColor, time.Now().Add(time.Millisecond*120))
panel.update(newColor, time.Now().Add(time.Millisecond*220))
} }
} else { } else {
panel.update([4]byte{0, 0, 0, 0}, time.Now()) panel.update([4]byte{0, 0, 0, 0}, time.Now())

5
internal/mysql/scenerepo.go

@ -2,6 +2,7 @@ package mysql
import ( import (
"context" "context"
"database/sql"
"encoding/json" "encoding/json"
"git.aiterp.net/lucifer/new-server/models" "git.aiterp.net/lucifer/new-server/models"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
@ -32,6 +33,10 @@ func (r *SceneRepo) FetchAll(ctx context.Context) ([]models.Scene, error) {
scenes := make([]sceneRecord, 0, 8) scenes := make([]sceneRecord, 0, 8)
err := r.DBX.SelectContext(ctx, &scenes, "SELECT * FROM scene") err := r.DBX.SelectContext(ctx, &scenes, "SELECT * FROM scene")
if err != nil { if err != nil {
if err == sql.ErrNoRows {
return []models.Scene{}, nil
}
return nil, dbErr(err) return nil, dbErr(err)
} }

6
models/scene.go

@ -56,7 +56,7 @@ func (s *Scene) RoleIndex(device *Device) int {
type SceneEffect string type SceneEffect string
const ( const (
SEStatic SceneEffect = "Plain"
SEStatic SceneEffect = "Static"
SERandom SceneEffect = "Random" SERandom SceneEffect = "Random"
SEGradient SceneEffect = "Gradient" SEGradient SceneEffect = "Gradient"
SEWalkingGradient SceneEffect = "WalkingGradient" SEWalkingGradient SceneEffect = "WalkingGradient"
@ -120,7 +120,7 @@ func (r *SceneRole) Validate() error {
} }
switch r.Effect { switch r.Effect {
case SEStatic, SERandom, SEGradient, SETransition:
case SEStatic, SERandom, SEGradient, SEWalkingGradient, SETransition:
default: default:
return ErrSceneRoleUnknownEffect return ErrSceneRoleUnknownEffect
} }
@ -201,8 +201,6 @@ func (r *SceneRole) State(fac float64) NewDeviceState {
return r.States[0] return r.States[0]
} }
if r.Interpolate { if r.Interpolate {
if len(r.States) == 2 { if len(r.States) == 2 {
return r.States[0].Interpolate(r.States[1], fac) return r.States[0].Interpolate(r.States[1], fac)

63
scene-examples/evening.yaml

@ -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.6'
intensity: 0.2
- effect: Static
power_mode: Device
target_kind: All
target_value: ""
interpolate: true
relative: false
order: +name
states:
- color: 'k:2000'
intensity: 0.25

9
scene-examples/flash.yaml

@ -0,0 +1,9 @@
name: Flash
interval: 0
roles:
- effect: Static
power_mode: Device
target_kind: All
target_value: ""
states:
- intensity: 1

48
scene-examples/late.yaml

@ -0,0 +1,48 @@
name: Late
interval: 5000
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:25,1'
intensity: 0.10
- effect: Gradient
power_mode: Device
target_kind: Tag
target_value: Square
interpolate: true
relative: false
order: +name
states:
- color: 'hs:25,1'
intensity: 0.05
- color: 'hs:35,1'
intensity: 0.05
- effect: Static
power_mode: Device
target_kind: Tag
target_value: Nightstand
interpolate: true
relative: false
order: +name
states:
- color: 'hs:25,1'
intensity: 0.05
- effect: Static
power_mode: Scene
target_kind: All
target_value: ''
interpolate: true
relative: false
states:
- power: false

40
scene-examples/morning.yaml

@ -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.5'
intensity: 0.50
- color: 'hs:220,0.4'
intensity: 0.65
- 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
Loading…
Cancel
Save