Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01975c74a9 | ||
|
|
cdf5173503 | ||
|
|
def494ba45 | ||
|
|
1b7e495a59 | ||
|
|
07173e4e56 | ||
|
|
d91bd472dd | ||
|
|
251687b85d |
+60
-25
@@ -3,9 +3,9 @@ package api
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"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"
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -17,11 +17,21 @@ func fetchDevices(ctx context.Context, fetchStr string) ([]models.Device, error)
|
|||||||
|
|
||||||
func Devices(r gin.IRoutes) {
|
func Devices(r gin.IRoutes) {
|
||||||
r.GET("", handler(func(c *gin.Context) (interface{}, error) {
|
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) {
|
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) {
|
r.PUT("", handler(func(c *gin.Context) (interface{}, error) {
|
||||||
@@ -72,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) {
|
r.PUT("/:fetch", handler(func(c *gin.Context) (interface{}, error) {
|
||||||
@@ -100,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) {
|
r.PUT("/:fetch/state", handler(func(c *gin.Context) (interface{}, error) {
|
||||||
@@ -128,16 +138,13 @@ func Devices(r gin.IRoutes) {
|
|||||||
config.PublishChannel <- devices
|
config.PublishChannel <- devices
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for _, device := range devices {
|
err = config.DeviceRepository().SaveMany(ctxOf(c), models.SMState, devices)
|
||||||
err := config.DeviceRepository().Save(context.Background(), &device, models.SMState)
|
if err != nil {
|
||||||
if err != nil {
|
log.Println("Failed to save devices states")
|
||||||
log.Println("Failed to save device for state:", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return devices, nil
|
return withSceneState(devices), nil
|
||||||
}))
|
}))
|
||||||
|
|
||||||
r.PUT("/:fetch/tags", handler(func(c *gin.Context) (interface{}, error) {
|
r.PUT("/:fetch/tags", handler(func(c *gin.Context) (interface{}, error) {
|
||||||
@@ -194,7 +201,7 @@ func Devices(r gin.IRoutes) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return devices, nil
|
return withSceneState(devices), nil
|
||||||
}))
|
}))
|
||||||
|
|
||||||
r.PUT("/:fetch/scene", handler(func(c *gin.Context) (interface{}, error) {
|
r.PUT("/:fetch/scene", handler(func(c *gin.Context) (interface{}, error) {
|
||||||
@@ -221,25 +228,53 @@ func Devices(r gin.IRoutes) {
|
|||||||
}
|
}
|
||||||
body.StartTime = time.Now()
|
body.StartTime = time.Now()
|
||||||
|
|
||||||
|
pushMode := c.Query("push") == "true"
|
||||||
for i := range devices {
|
for i := range devices {
|
||||||
devices[i].SceneAssignments = []models.DeviceSceneAssignment{body}
|
if pushMode {
|
||||||
|
devices[i].SceneAssignments = append(devices[i].SceneAssignments, body)
|
||||||
|
} else {
|
||||||
|
devices[i].SceneAssignments = []models.DeviceSceneAssignment{body}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
config.PublishChannel <- devices
|
config.PublishChannel <- devices
|
||||||
|
|
||||||
eg := errgroup.Group{}
|
err = config.DeviceRepository().SaveMany(ctxOf(c), 0, devices)
|
||||||
for i := range devices {
|
|
||||||
device := devices[i]
|
|
||||||
|
|
||||||
eg.Go(func() error {
|
|
||||||
return config.DeviceRepository().Save(ctxOf(c), &device, 0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
err = eg.Wait()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return devices, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
+17
-6
@@ -35,17 +35,28 @@ func EventHandlers(r gin.IRoutes) {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
r.PUT("/:id", handler(func(c *gin.Context) (interface{}, error) {
|
r.PUT("/:id", handler(func(c *gin.Context) (interface{}, error) {
|
||||||
var body models.EventHandlerUpdate
|
|
||||||
err := parseBody(c, &body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
handler, err := config.EventHandlerRepository().FindByID(ctxOf(c), intParam(c, "id"))
|
handler, err := config.EventHandlerRepository().FindByID(ctxOf(c), intParam(c, "id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
handler.ApplyUpdate(body)
|
|
||||||
|
if c.Query("hard") == "true" {
|
||||||
|
var body models.EventHandler
|
||||||
|
err := parseBody(c, &body)
|
||||||
|
if err != nil || body.ID != intParam(c, "id") {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
handler = &body
|
||||||
|
} else {
|
||||||
|
var body models.EventHandlerUpdate
|
||||||
|
err := parseBody(c, &body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
handler.ApplyUpdate(body)
|
||||||
|
}
|
||||||
|
|
||||||
err = config.EventHandlerRepository().Save(ctxOf(c), handler)
|
err = config.EventHandlerRepository().Save(ctxOf(c), handler)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+32
-1
@@ -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
|
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,31 @@ 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, 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 {
|
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,6 +16,26 @@ func (client *Client) GetHandlers(ctx context.Context) ([]models.EventHandler, e
|
|||||||
return handlers, nil
|
return handlers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (client *Client) GetHandler(ctx context.Context, id int) (*models.EventHandler, error) {
|
||||||
|
var handler models.EventHandler
|
||||||
|
err := client.Fetch(ctx, "GET", fmt.Sprintf("/api/event-handlers/%d", id), &handler, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &handler, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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?hard=true", handler.ID), &response, handler)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (client *Client) AddHandler(ctx context.Context, handler models.EventHandler) (*models.EventHandler, error) {
|
func (client *Client) AddHandler(ctx context.Context, handler models.EventHandler) (*models.EventHandler, error) {
|
||||||
var result models.EventHandler
|
var result models.EventHandler
|
||||||
err := client.Fetch(ctx, "POST", "/api/event-handlers", &result, handler)
|
err := client.Fetch(ctx, "POST", "/api/event-handlers", &result, handler)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"git.aiterp.net/lucifer/new-server/app/config"
|
"git.aiterp.net/lucifer/new-server/app/config"
|
||||||
"git.aiterp.net/lucifer/new-server/models"
|
"git.aiterp.net/lucifer/new-server/models"
|
||||||
"log"
|
"log"
|
||||||
"strconv"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -53,15 +52,6 @@ func runConnectToBridges() error {
|
|||||||
log.Printf("Running bridge \"%s\" (%d)", bridge.Name, bridge.ID)
|
log.Printf("Running bridge \"%s\" (%d)", bridge.Name, bridge.ID)
|
||||||
|
|
||||||
go func(bridge models.Bridge, cancel func()) {
|
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)
|
err = driver.Run(ctx, bridge, config.EventChannel)
|
||||||
log.Printf("Bridge \"%s\" (%d) stopped: %s", bridge.Name, bridge.ID, err)
|
log.Printf("Bridge \"%s\" (%d) stopped: %s", bridge.Name, bridge.ID, err)
|
||||||
|
|
||||||
|
|||||||
@@ -155,6 +155,15 @@ func handleEvent(event models.Event) (responses []models.Event) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Error updating state for device", device.ID, "err:", err)
|
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
|
config.PublishChannel <- allDevices
|
||||||
|
|||||||
@@ -19,6 +19,29 @@ type Publisher struct {
|
|||||||
waiting map[int]chan struct{}
|
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 {
|
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 +49,12 @@ func (p *Publisher) ReloadScenes(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
for _, scene := range scenes {
|
for i, scene := range scenes {
|
||||||
p.sceneData[scene.ID] = &scene
|
p.sceneData[scene.ID] = &scenes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, scene := range p.scenes {
|
||||||
|
scene.UpdateScene(*p.sceneData[scene.data.ID])
|
||||||
}
|
}
|
||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
|
|
||||||
@@ -83,16 +110,21 @@ func (p *Publisher) Run() {
|
|||||||
ticker := time.NewTicker(time.Millisecond * 100)
|
ticker := time.NewTicker(time.Millisecond * 100)
|
||||||
deleteList := make([]int, 0, 8)
|
deleteList := make([]int, 0, 8)
|
||||||
updatedList := make([]models.Device, 0, 16)
|
updatedList := make([]models.Device, 0, 16)
|
||||||
|
reassignList := make([]models.Device, 0, 16)
|
||||||
|
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
deleteList = deleteList[:0]
|
deleteList = deleteList[:0]
|
||||||
updatedList = updatedList[:0]
|
updatedList = updatedList[:0]
|
||||||
|
reassignList = reassignList[:0]
|
||||||
|
|
||||||
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()...)
|
||||||
|
reassignList = append(reassignList, 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
|
||||||
@@ -109,7 +141,15 @@ func (p *Publisher) Run() {
|
|||||||
for _, i := range deleteList {
|
for _, i := range deleteList {
|
||||||
p.scenes = append(p.scenes[:i], p.scenes[i+1:]...)
|
p.scenes = append(p.scenes[:i], p.scenes[i+1:]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, device := range reassignList {
|
||||||
|
p.reassignDevice(device)
|
||||||
|
|
||||||
|
if p.sceneAssignment[device.ID] != nil {
|
||||||
|
p.sceneAssignment[device.ID].UpsertDevice(device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, device := range updatedList {
|
for _, device := range updatedList {
|
||||||
if !p.started[device.BridgeID] {
|
if !p.started[device.BridgeID] {
|
||||||
p.started[device.BridgeID] = true
|
p.started[device.BridgeID] = true
|
||||||
@@ -130,10 +170,10 @@ func (p *Publisher) Run() {
|
|||||||
// should trigger an update.
|
// should trigger an update.
|
||||||
func (p *Publisher) reassignDevice(device models.Device) bool {
|
func (p *Publisher) reassignDevice(device models.Device) bool {
|
||||||
var selectedAssignment *models.DeviceSceneAssignment
|
var selectedAssignment *models.DeviceSceneAssignment
|
||||||
for _, assignment := range device.SceneAssignments {
|
for i, assignment := range device.SceneAssignments {
|
||||||
duration := time.Duration(assignment.DurationMS) * time.Millisecond
|
duration := time.Duration(assignment.DurationMS) * time.Millisecond
|
||||||
if duration <= 0 || time.Now().Before(assignment.StartTime.Add(duration)) {
|
if duration <= 0 || time.Now().Before(assignment.StartTime.Add(duration)) {
|
||||||
selectedAssignment = &assignment
|
selectedAssignment = &device.SceneAssignments[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +188,11 @@ func (p *Publisher) reassignDevice(device models.Device) bool {
|
|||||||
|
|
||||||
// Stop here, no scene should be assigned.
|
// Stop here, no scene should be assigned.
|
||||||
return false
|
return false
|
||||||
|
} else {
|
||||||
|
if p.sceneData[selectedAssignment.SceneID] == nil {
|
||||||
|
// Freeze until scene becomes available.
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.sceneAssignment[device.ID] != nil {
|
if p.sceneAssignment[device.ID] != nil {
|
||||||
@@ -175,6 +220,7 @@ func (p *Publisher) reassignDevice(device models.Device) bool {
|
|||||||
endTime: selectedAssignment.StartTime.Add(time.Duration(selectedAssignment.DurationMS) * time.Millisecond),
|
endTime: selectedAssignment.StartTime.Add(time.Duration(selectedAssignment.DurationMS) * time.Millisecond),
|
||||||
roleMap: make(map[int]int, 16),
|
roleMap: make(map[int]int, 16),
|
||||||
roleList: make(map[int][]models.Device, 16),
|
roleList: make(map[int][]models.Device, 16),
|
||||||
|
lastStates: make(map[int]models.DeviceState, 16),
|
||||||
due: true,
|
due: true,
|
||||||
}
|
}
|
||||||
p.sceneAssignment[device.ID] = newScene
|
p.sceneAssignment[device.ID] = newScene
|
||||||
@@ -258,8 +304,12 @@ 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 {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -268,8 +318,9 @@ func Initialize(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
go publisher.PublishChannel(config.PublishChannel)
|
|
||||||
go publisher.Run()
|
go publisher.Run()
|
||||||
|
time.Sleep(time.Millisecond * 50)
|
||||||
|
go publisher.PublishChannel(config.PublishChannel)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Scene struct {
|
type Scene struct {
|
||||||
data *models.Scene
|
data *models.Scene
|
||||||
group string
|
group string
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
endTime time.Time
|
endTime time.Time
|
||||||
roleMap map[int]int
|
roleMap map[int]int
|
||||||
roleList map[int][]models.Device
|
roleList map[int][]models.Device
|
||||||
|
lastStates map[int]models.DeviceState
|
||||||
|
|
||||||
due bool
|
due bool
|
||||||
lastInterval int64
|
lastInterval int64
|
||||||
@@ -84,6 +85,17 @@ func (s *Scene) RemoveDevice(device models.Device) {
|
|||||||
s.due = true
|
s.due = true
|
||||||
|
|
||||||
delete(s.roleMap, device.ID)
|
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 {
|
func (s *Scene) Due() bool {
|
||||||
@@ -152,6 +164,8 @@ func (s *Scene) Run() []models.Device {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.lastStates[device.ID] = device.State
|
||||||
|
|
||||||
updatedDevices = append(updatedDevices, device)
|
updatedDevices = append(updatedDevices, device)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,3 +175,12 @@ func (s *Scene) Run() []models.Device {
|
|||||||
|
|
||||||
return updatedDevices
|
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"
|
"log"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Param struct {
|
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 {
|
type Command struct {
|
||||||
Name string
|
Name string
|
||||||
|
|||||||
+118
-41
@@ -7,6 +7,7 @@ import (
|
|||||||
"git.aiterp.net/lucifer/new-server/models"
|
"git.aiterp.net/lucifer/new-server/models"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handlerCmd(
|
func handlerCmd(
|
||||||
@@ -38,47 +39,7 @@ func handlerCmd(
|
|||||||
log.Fatalln("Search filter needed!")
|
log.Fatalln("Search filter needed!")
|
||||||
}
|
}
|
||||||
model.TargetKind, model.TargetValue = models.ParseFetchString(*fetchStr)
|
model.TargetKind, model.TargetValue = models.ParseFetchString(*fetchStr)
|
||||||
|
model = applyCmdToHandler(model, cmd)
|
||||||
// From / To
|
|
||||||
model.From = cmd.Params.Get("from").TimeOfDayOr(models.Never)
|
|
||||||
model.To = cmd.Params.Get("to").TimeOfDayOr(models.Never)
|
|
||||||
if model.From.IsNever() != model.To.IsNever() {
|
|
||||||
log.Fatalln("from and to must be specified together!")
|
|
||||||
}
|
|
||||||
|
|
||||||
// One shot
|
|
||||||
model.OneShot = cmd.Params.Get("one-shot").StringOr("false") == "true"
|
|
||||||
|
|
||||||
// Priority
|
|
||||||
model.Priority = cmd.Params.Get("priority").IntOr(100)
|
|
||||||
|
|
||||||
// Conditions
|
|
||||||
model.Conditions = cmd.Params.Subset("conditions").EventConditions()
|
|
||||||
|
|
||||||
// Power, Color, Intensity, Temperature
|
|
||||||
nds := cmd.Params.DeviceState("set-")
|
|
||||||
model.Actions.SetPower = nds.Power
|
|
||||||
model.Actions.SetColor = nds.Color
|
|
||||||
model.Actions.SetIntensity = nds.Intensity
|
|
||||||
|
|
||||||
// Add intensity
|
|
||||||
model.Actions.AddIntensity = cmd.Params.Get("add-intensity").Float()
|
|
||||||
|
|
||||||
// Fire
|
|
||||||
fireParams := cmd.Params.Subset("fire")
|
|
||||||
fireName := fireParams.Get("name").String()
|
|
||||||
if fireName != nil {
|
|
||||||
firePayloadParams := fireParams.Subset("payload")
|
|
||||||
|
|
||||||
model.Actions.FireEvent = &models.Event{
|
|
||||||
Name: *fireName,
|
|
||||||
Payload: make(map[string]string, len(firePayloadParams)),
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, value := range firePayloadParams.StringMap() {
|
|
||||||
model.Actions.FireEvent.Payload[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newHandler, err := c.AddHandler(ctx, model)
|
newHandler, err := c.AddHandler(ctx, model)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -86,6 +47,34 @@ func handlerCmd(
|
|||||||
}
|
}
|
||||||
|
|
||||||
WriteHandlerInfoTable(os.Stdout, []models.EventHandler{*newHandler})
|
WriteHandlerInfoTable(os.Stdout, []models.EventHandler{*newHandler})
|
||||||
|
case "edit":
|
||||||
|
id := cmd.Params.Get(0).IntOr(0)
|
||||||
|
if id == 0 {
|
||||||
|
log.Fatalln("ID missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
model, err := c.GetHandler(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
eventStr := cmd.Params.Get("event").String()
|
||||||
|
if eventStr != nil {
|
||||||
|
model.EventName = *eventStr
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchStr := cmd.Params.Get("search").String()
|
||||||
|
if fetchStr != nil {
|
||||||
|
model.TargetKind, model.TargetValue = models.ParseFetchString(*fetchStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
modified := applyCmdToHandler(*model, cmd)
|
||||||
|
returned, err := c.PutHandler(ctx, &modified)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteHandlerInfoTable(os.Stdout, []models.EventHandler{*returned})
|
||||||
case "delete":
|
case "delete":
|
||||||
id := cmd.Params.Get(0).Int()
|
id := cmd.Params.Get(0).Int()
|
||||||
if id == nil {
|
if id == nil {
|
||||||
@@ -106,3 +95,91 @@ func handlerCmd(
|
|||||||
_, _ = fmt.Fprintln(os.Stderr, helpString[1:])
|
_, _ = fmt.Fprintln(os.Stderr, helpString[1:])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applyCmdToHandler(model models.EventHandler, cmd Command) models.EventHandler {
|
||||||
|
// Remove keys
|
||||||
|
for _, elem := range cmd.Params.Strings(1) {
|
||||||
|
if !strings.HasPrefix(elem, "-") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keyToRemove := elem[1:]
|
||||||
|
|
||||||
|
if strings.HasPrefix(keyToRemove, "conditions.") {
|
||||||
|
condKeyToRemove := keyToRemove[11:]
|
||||||
|
delete(model.Conditions, condKeyToRemove)
|
||||||
|
} else if strings.HasPrefix(keyToRemove, "fire.payload.") && model.Actions.FireEvent != nil {
|
||||||
|
firePayloadKeyToRemove := keyToRemove[13:]
|
||||||
|
delete(model.Actions.FireEvent.Payload, firePayloadKeyToRemove)
|
||||||
|
} else {
|
||||||
|
switch keyToRemove {
|
||||||
|
case "set-power":
|
||||||
|
model.Actions.SetPower = nil
|
||||||
|
case "set-color":
|
||||||
|
model.Actions.SetColor = nil
|
||||||
|
case "set-intensity":
|
||||||
|
model.Actions.SetIntensity = nil
|
||||||
|
case "add-intensity":
|
||||||
|
model.Actions.AddIntensity = nil
|
||||||
|
case "fire":
|
||||||
|
fallthrough
|
||||||
|
case "fire.name":
|
||||||
|
model.Actions.FireEvent = nil
|
||||||
|
case "from":
|
||||||
|
model.From = models.Never
|
||||||
|
case "to":
|
||||||
|
model.To = models.Never
|
||||||
|
case "set-scene":
|
||||||
|
model.Actions.SetScene = nil
|
||||||
|
case "push-scene":
|
||||||
|
model.Actions.PushScene = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// From / To
|
||||||
|
model.From = cmd.Params.Get("from").TimeOfDayOr(models.Never)
|
||||||
|
model.To = cmd.Params.Get("to").TimeOfDayOr(models.Never)
|
||||||
|
if model.From.IsNever() != model.To.IsNever() {
|
||||||
|
log.Fatalln("from and to must be specified together!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// One shot
|
||||||
|
model.OneShot = cmd.Params.Get("one-shot").StringOr("false") == "true"
|
||||||
|
|
||||||
|
// Priority
|
||||||
|
model.Priority = cmd.Params.Get("priority").IntOr(100)
|
||||||
|
|
||||||
|
// Conditions
|
||||||
|
model.Conditions = cmd.Params.Subset("conditions").EventConditions()
|
||||||
|
|
||||||
|
// Power, Color, Intensity, Temperature
|
||||||
|
nds := cmd.Params.DeviceState("set-")
|
||||||
|
model.Actions.SetPower = nds.Power
|
||||||
|
model.Actions.SetColor = nds.Color
|
||||||
|
model.Actions.SetIntensity = nds.Intensity
|
||||||
|
|
||||||
|
// Add intensity
|
||||||
|
model.Actions.AddIntensity = cmd.Params.Get("add-intensity").Float()
|
||||||
|
|
||||||
|
// Fire
|
||||||
|
fireParams := cmd.Params.Subset("fire")
|
||||||
|
fireName := fireParams.Get("name").String()
|
||||||
|
if fireName != nil {
|
||||||
|
firePayloadParams := fireParams.Subset("payload")
|
||||||
|
|
||||||
|
model.Actions.FireEvent = &models.Event{
|
||||||
|
Name: *fireName,
|
||||||
|
Payload: make(map[string]string, len(firePayloadParams)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range firePayloadParams.StringMap() {
|
||||||
|
model.Actions.FireEvent.Payload[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scenes
|
||||||
|
model.Actions.SetScene = cmd.Params.SceneAssignment("set-")
|
||||||
|
model.Actions.PushScene = cmd.Params.SceneAssignment("push-")
|
||||||
|
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ EVENT HANDLER COMMANDS
|
|||||||
<set-power=B> <set-color=C> <set-intensity=F> <set-temperature=N> \
|
<set-power=B> <set-color=C> <set-intensity=F> <set-temperature=N> \
|
||||||
<add-intensity=F> \
|
<add-intensity=F> \
|
||||||
<fire.name=S> <fire.payload.*=S>
|
<fire.name=S> <fire.payload.*=S>
|
||||||
|
handler edit <id> \
|
||||||
|
(...all arguments from 'add', appending conditions and fire payload matchers...) \
|
||||||
|
<event=S> <search=S>
|
||||||
|
<-from> <-to> \
|
||||||
|
<-set-power> <-set-color> <-set-intensity> <-set-temperature> \
|
||||||
|
<-conditions.*> \
|
||||||
|
<-fire> <-fire.payload.*>
|
||||||
handler delete <id>
|
handler delete <id>
|
||||||
|
|
||||||
EVENT COMMANDS
|
EVENT COMMANDS
|
||||||
|
|||||||
@@ -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":
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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.SetHeader([]string{"ID", "NAME", "POWER", "COLOR", "INTENSITY", "TEMPERATURE"})
|
||||||
|
|
||||||
table.SetReflowDuringAutoWrap(true)
|
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 {
|
for _, v := range devices {
|
||||||
powerStr := ""
|
powerStr := ""
|
||||||
if v.HasCapability(models.DCPower) {
|
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 := ""
|
colorStr := ""
|
||||||
if v.HasCapability(models.DCColorHSK, models.DCColorHS, models.DCColorKelvin) {
|
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 := ""
|
temperatureString := ""
|
||||||
@@ -33,7 +49,11 @@ func WriteDeviceStateTable(w io.Writer, devices []models.Device) {
|
|||||||
|
|
||||||
intensityString := ""
|
intensityString := ""
|
||||||
if v.HasCapability(models.DCIntensity) {
|
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{
|
table.Append([]string{
|
||||||
@@ -104,6 +124,17 @@ func WriteHandlerInfoTable(w io.Writer, handlers []models.EventHandler) {
|
|||||||
if h.Actions.FireEvent != nil {
|
if h.Actions.FireEvent != nil {
|
||||||
actionStr += fmt.Sprintf("fireEvent=%s ", (*h.Actions.FireEvent).Name)
|
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
|
eventName := h.EventName
|
||||||
if h.OneShot {
|
if h.OneShot {
|
||||||
|
|||||||
@@ -122,6 +122,9 @@ func (state *hueSensorState) Update(newData SensorData) *models.Event {
|
|||||||
if time.Since(stateTime) > time.Second*3 {
|
if time.Since(stateTime) > time.Second*3 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if state.prevData == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
pe := state.prevData.State.ButtonEvent
|
pe := state.prevData.State.ButtonEvent
|
||||||
ce := newData.State.ButtonEvent
|
ce := newData.State.ButtonEvent
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,9 +26,11 @@ func (c *ColorValue) String() string {
|
|||||||
return fmt.Sprintf("k:%d", c.Kelvin)
|
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) {
|
func ParseColorValue(raw string) (ColorValue, error) {
|
||||||
tokens := strings.SplitN(raw, ":", 2)
|
tokens := strings.SplitN(raw, ":", 2)
|
||||||
if len(tokens) != 2 {
|
if len(tokens) != 2 {
|
||||||
|
|||||||
+2
-1
@@ -18,6 +18,7 @@ type Device struct {
|
|||||||
DriverProperties map[string]interface{} `json:"driverProperties"`
|
DriverProperties map[string]interface{} `json:"driverProperties"`
|
||||||
UserProperties map[string]string `json:"userProperties"`
|
UserProperties map[string]string `json:"userProperties"`
|
||||||
SceneAssignments []DeviceSceneAssignment `json:"sceneAssignments"`
|
SceneAssignments []DeviceSceneAssignment `json:"sceneAssignments"`
|
||||||
|
SceneState *DeviceState `json:"sceneState"`
|
||||||
State DeviceState `json:"state"`
|
State DeviceState `json:"state"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
}
|
}
|
||||||
@@ -200,7 +201,7 @@ func (s *NewDeviceState) RelativeTo(device Device) NewDeviceState {
|
|||||||
if s.Color != nil {
|
if s.Color != nil {
|
||||||
c, err := ParseColorValue(*s.Color)
|
c, err := ParseColorValue(*s.Color)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.Hue = math.Mod(device.State.Color.Hue + c.Hue, 360)
|
c.Hue = math.Mod(device.State.Color.Hue+c.Hue, 360)
|
||||||
c.Saturation *= device.State.Color.Saturation
|
c.Saturation *= device.State.Color.Saturation
|
||||||
c.Kelvin += device.State.Color.Kelvin
|
c.Kelvin += device.State.Color.Kelvin
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-6
@@ -5,6 +5,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EventHandler struct {
|
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
|
return c.matches(strconv.FormatFloat(device.State.Temperature, 'f', -1, 64)), false
|
||||||
|
case "scene":
|
||||||
|
if len(device.SceneAssignments) == 0 {
|
||||||
|
return c.matches("-1"), 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:
|
default:
|
||||||
return false, true
|
return false, true
|
||||||
}
|
}
|
||||||
@@ -214,12 +229,14 @@ func (c *EventCondition) matches(value string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type EventAction struct {
|
type EventAction struct {
|
||||||
SetPower *bool `json:"setPower"`
|
SetPower *bool `json:"setPower"`
|
||||||
SetColor *string `json:"setColor"`
|
SetColor *string `json:"setColor"`
|
||||||
SetIntensity *float64 `json:"setIntensity"`
|
SetIntensity *float64 `json:"setIntensity"`
|
||||||
SetTemperature *int `json:"setTemperature"`
|
SetTemperature *int `json:"setTemperature"`
|
||||||
AddIntensity *float64 `json:"addIntensity"`
|
AddIntensity *float64 `json:"addIntensity"`
|
||||||
FireEvent *Event `json:"fireEvent"`
|
FireEvent *Event `json:"fireEvent"`
|
||||||
|
SetScene *DeviceSceneAssignment `json:"setScene"`
|
||||||
|
PushScene *DeviceSceneAssignment `json:"pushScene"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (action *EventAction) Apply(other EventAction) {
|
func (action *EventAction) Apply(other EventAction) {
|
||||||
@@ -238,6 +255,12 @@ func (action *EventAction) Apply(other EventAction) {
|
|||||||
if action.FireEvent == nil {
|
if action.FireEvent == nil {
|
||||||
action.FireEvent = other.FireEvent
|
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 {
|
func (c EventCondition) String() string {
|
||||||
|
|||||||
+10
-6
@@ -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"
|
||||||
@@ -114,13 +114,13 @@ func (r *SceneRole) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch r.PowerMode {
|
switch r.PowerMode {
|
||||||
case SPScene, SPDevice:
|
case SPScene, SPDevice, SPBoth:
|
||||||
default:
|
default:
|
||||||
return ErrSceneRoleUnknownPowerMode
|
return ErrSceneRoleUnknownPowerMode
|
||||||
}
|
}
|
||||||
|
|
||||||
switch r.Effect {
|
switch r.Effect {
|
||||||
case SEStatic, SERandom, SEGradient, SETransition:
|
case SEStatic, SERandom, SEGradient, SEWalkingGradient, SETransition:
|
||||||
default:
|
default:
|
||||||
return ErrSceneRoleUnknownEffect
|
return ErrSceneRoleUnknownEffect
|
||||||
}
|
}
|
||||||
@@ -188,7 +188,12 @@ func (r *SceneRole) ApplyEffect(device *Device, c SceneRunContext) (newState New
|
|||||||
case SPDevice:
|
case SPDevice:
|
||||||
newState.Power = nil
|
newState.Power = nil
|
||||||
case SPScene:
|
case SPScene:
|
||||||
// Do nothing
|
// Do nothing
|
||||||
|
case SPBoth:
|
||||||
|
if newState.Power != nil {
|
||||||
|
powerIntersection := *newState.Power && device.State.Power
|
||||||
|
newState.Power = &powerIntersection
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -201,8 +206,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)
|
||||||
@@ -228,6 +231,7 @@ type ScenePowerMode string
|
|||||||
const (
|
const (
|
||||||
SPDevice ScenePowerMode = "Device" // Device state decides power. Scene state may only power off.
|
SPDevice ScenePowerMode = "Device" // Device state decides power. Scene state may only power off.
|
||||||
SPScene ScenePowerMode = "Scene" // Scene state decide power.
|
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
|
// DeviceSceneAssignment is an entry on the device stack. StartTime and DurationMS are only respected when the scene
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
Reference in New Issue
Block a user