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).
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
+32
-1
@@ -2,6 +2,7 @@ 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"
|
||||
)
|
||||
@@ -32,6 +33,36 @@ func Scenes(r gin.IRoutes) {
|
||||
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
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -60,6 +60,16 @@ func (client *Client) PutDeviceTags(ctx context.Context, fetchStr string, addTag
|
||||
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 {
|
||||
err := client.Fetch(ctx, "POST", "/api/events", nil, event)
|
||||
if err != nil {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -19,6 +19,18 @@ type Publisher 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 {
|
||||
scenes, err := config.SceneRepository().FetchAll(ctx)
|
||||
if err != nil {
|
||||
@@ -26,8 +38,12 @@ func (p *Publisher) ReloadScenes(ctx context.Context) error {
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -90,10 +106,12 @@ func (p *Publisher) Run() {
|
||||
|
||||
p.mu.Lock()
|
||||
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))
|
||||
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
|
||||
}
|
||||
@@ -258,6 +276,10 @@ var publisher = Publisher{
|
||||
waiting: make(map[int]chan struct{}),
|
||||
}
|
||||
|
||||
func Global() *Publisher {
|
||||
return &publisher
|
||||
}
|
||||
|
||||
func Initialize(ctx context.Context) error {
|
||||
err :=publisher.ReloadScenes(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -86,6 +86,17 @@ func (s *Scene) RemoveDevice(device models.Device) {
|
||||
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 {
|
||||
if s.due {
|
||||
return true
|
||||
|
||||
@@ -96,6 +96,9 @@ func main() {
|
||||
case "handler":
|
||||
handlerCmd(ctx, c)
|
||||
|
||||
case "scene":
|
||||
sceneCmd(ctx, c)
|
||||
|
||||
// EVENT
|
||||
case "run":
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -2,6 +2,7 @@ package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"git.aiterp.net/lucifer/new-server/models"
|
||||
"github.com/jmoiron/sqlx"
|
||||
@@ -32,6 +33,10 @@ 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)
|
||||
}
|
||||
|
||||
|
||||
+2
-4
@@ -56,7 +56,7 @@ func (s *Scene) RoleIndex(device *Device) int {
|
||||
type SceneEffect string
|
||||
|
||||
const (
|
||||
SEStatic SceneEffect = "Plain"
|
||||
SEStatic SceneEffect = "Static"
|
||||
SERandom SceneEffect = "Random"
|
||||
SEGradient SceneEffect = "Gradient"
|
||||
SEWalkingGradient SceneEffect = "WalkingGradient"
|
||||
@@ -120,7 +120,7 @@ func (r *SceneRole) Validate() error {
|
||||
}
|
||||
|
||||
switch r.Effect {
|
||||
case SEStatic, SERandom, SEGradient, SETransition:
|
||||
case SEStatic, SERandom, SEGradient, SEWalkingGradient, SETransition:
|
||||
default:
|
||||
return ErrSceneRoleUnknownEffect
|
||||
}
|
||||
@@ -201,8 +201,6 @@ func (r *SceneRole) State(fac float64) NewDeviceState {
|
||||
return r.States[0]
|
||||
}
|
||||
|
||||
|
||||
|
||||
if r.Interpolate {
|
||||
if len(r.States) == 2 {
|
||||
return r.States[0].Interpolate(r.States[1], fac)
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,9 @@
|
||||
name: Flash
|
||||
interval: 0
|
||||
roles:
|
||||
- effect: Static
|
||||
power_mode: Device
|
||||
target_kind: All
|
||||
target_value: ""
|
||||
states:
|
||||
- intensity: 1
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user