Browse Source

scene stuff is ready for prime time now.

pull/1/head
Gisle Aune 3 years ago
parent
commit
def494ba45
  1. 79
      app/api/devices.go
  2. 19
      app/client/client.go
  3. 2
      app/client/handler.go
  4. 10
      app/services/bridges.go
  5. 9
      app/services/events.go
  6. 19
      app/services/publisher/publisher.go
  7. 26
      app/services/publisher/scene.go
  8. 12
      cmd/lucy/command.go
  9. 10
      cmd/lucy/handlercmd.go
  10. 18
      cmd/lucy/scenecmd.go
  11. 37
      cmd/lucy/tables.go
  12. 4
      models/colorvalue.go
  13. 3
      models/device.go
  14. 35
      models/eventhandler.go
  15. 10
      models/scene.go
  16. 6
      scene-examples/evening.yaml
  17. 19
      scene-examples/flash.yaml
  18. 23
      scene-examples/late.yaml
  19. 4
      scene-examples/morning.yaml
  20. 42
      scene-examples/saturday.yaml

79
app/api/devices.go

@ -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().Save(context.Background(), &device, models.SMState)
if err != nil {
log.Println("Failed to save device for state:", err)
continue
}
err = config.DeviceRepository().SaveMany(ctxOf(c), models.SMState, devices)
if err != nil {
log.Println("Failed to save devices states")
} }
}() }()
return devices, nil
return withSceneState(devices), nil
})) }))
r.PUT("/:fetch/tags", handler(func(c *gin.Context) (interface{}, error) { 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{}
for i := range devices {
device := devices[i]
err = config.DeviceRepository().SaveMany(ctxOf(c), 0, devices)
if err != nil {
return nil, err
}
eg.Go(func() error {
return config.DeviceRepository().Save(ctxOf(c), &device, 0)
})
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 = eg.Wait()
err = config.DeviceRepository().SaveMany(ctxOf(c), 0, devices)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return devices, nil
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
}

19
app/client/client.go

@ -60,9 +60,24 @@ 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) {
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) devices := make([]models.Device, 0, 16)
err := client.Fetch(ctx, "PUT", "/api/devices/"+fetchStr+"/scene", &devices, assignment)
err := client.Fetch(ctx, "DELETE", "/api/devices/"+fetchStr+"/scene", &devices, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }

2
app/client/handler.go

@ -28,7 +28,7 @@ func (client *Client) GetHandler(ctx context.Context, id int) (*models.EventHand
func (client *Client) PutHandler(ctx context.Context, handler *models.EventHandler) (*models.EventHandler, error) { func (client *Client) PutHandler(ctx context.Context, handler *models.EventHandler) (*models.EventHandler, error) {
var response models.EventHandler var response models.EventHandler
err := client.Fetch(ctx, "PUT", fmt.Sprintf("/api/event-handlers/%d", handler.ID), &response, handler)
err := client.Fetch(ctx, "PUT", fmt.Sprintf("/api/event-handlers/%d?hard=true", handler.ID), &response, handler)
if err != nil { if err != nil {
return nil, err return nil, err
} }

10
app/services/bridges.go

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

9
app/services/events.go

@ -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
app/services/publisher/publisher.go

@ -19,6 +19,17 @@ 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) { func (p *Publisher) UpdateScene(data models.Scene) {
p.mu.Lock() p.mu.Lock()
p.sceneData[data.ID] = &data p.sceneData[data.ID] = &data
@ -127,7 +138,7 @@ 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 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
@ -193,6 +204,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
@ -281,7 +293,7 @@ func Global() *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
} }
@ -290,8 +302,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
} }

26
app/services/publisher/scene.go

@ -6,12 +6,13 @@ import (
) )
type Scene struct { type Scene struct {
data *models.Scene
group string
startTime time.Time
endTime time.Time
roleMap map[int]int
roleList map[int][]models.Device
data *models.Scene
group string
startTime time.Time
endTime time.Time
roleMap map[int]int
roleList map[int][]models.Device
lastStates map[int]models.DeviceState
due bool due bool
lastInterval int64 lastInterval int64
@ -84,6 +85,7 @@ 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 { func (s *Scene) Empty() bool {
@ -96,7 +98,6 @@ func (s *Scene) Empty() bool {
return true return true
} }
func (s *Scene) Due() bool { func (s *Scene) Due() bool {
if s.due { if s.due {
return true return true
@ -163,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)
} }
} }
@ -172,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
}

12
cmd/lucy/command.go

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

10
cmd/lucy/handlercmd.go

@ -99,7 +99,7 @@ func handlerCmd(
func applyCmdToHandler(model models.EventHandler, cmd Command) models.EventHandler { func applyCmdToHandler(model models.EventHandler, cmd Command) models.EventHandler {
// Remove keys // Remove keys
for _, elem := range cmd.Params.Strings(1) { for _, elem := range cmd.Params.Strings(1) {
if elem[0] != '-' {
if !strings.HasPrefix(elem, "-") {
continue continue
} }
keyToRemove := elem[1:] keyToRemove := elem[1:]
@ -128,6 +128,10 @@ func applyCmdToHandler(model models.EventHandler, cmd Command) models.EventHandl
model.From = models.Never model.From = models.Never
case "to": case "to":
model.To = models.Never model.To = models.Never
case "set-scene":
model.Actions.SetScene = nil
case "push-scene":
model.Actions.PushScene = nil
} }
} }
} }
@ -173,5 +177,9 @@ func applyCmdToHandler(model models.EventHandler, cmd Command) models.EventHandl
} }
} }
// Scenes
model.Actions.SetScene = cmd.Params.SceneAssignment("set-")
model.Actions.PushScene = cmd.Params.SceneAssignment("push-")
return model return model
} }

18
cmd/lucy/scenecmd.go

@ -79,7 +79,7 @@ func sceneCmd(
} }
} }
case "assign":
case "push", "assign":
{ {
fetch := cmd.Params.Get(0).String() fetch := cmd.Params.Get(0).String()
id := cmd.Params.Get(1).Int() id := cmd.Params.Get(1).Int()
@ -87,7 +87,7 @@ func sceneCmd(
log.Println("Usage: lucy scene assign <fetch> <id> <group=S> <duration=I>") log.Println("Usage: lucy scene assign <fetch> <id> <group=S> <duration=I>")
} }
devices, err := c.AssignDevice(ctx, *fetch, models.DeviceSceneAssignment{
devices, err := c.AssignDevice(ctx, *fetch, cmd.Name == "push", models.DeviceSceneAssignment{
SceneID: *id, SceneID: *id,
Group: cmd.Params.Get("group").StringOr(*fetch), Group: cmd.Params.Get("group").StringOr(*fetch),
DurationMS: int64(cmd.Params.Get("duration").IntOr(0)), DurationMS: int64(cmd.Params.Get("duration").IntOr(0)),
@ -99,6 +99,20 @@ func sceneCmd(
WriteDeviceInfoTable(os.Stdout, devices) 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)
} }
} }

37
cmd/lucy/tables.go

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

4
models/colorvalue.go

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

3
models/device.go

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

35
models/eventhandler.go

@ -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 false, false
}
sceneId := -1
for _, assignment := range device.SceneAssignments {
duration := time.Duration(assignment.DurationMS) * time.Millisecond
if duration <= 0 || time.Now().Before(assignment.StartTime.Add(duration)) {
sceneId = assignment.SceneID
}
}
return c.matches(strconv.Itoa(sceneId)), false
default: 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"`
SetColor *string `json:"setColor"`
SetIntensity *float64 `json:"setIntensity"`
SetTemperature *int `json:"setTemperature"`
AddIntensity *float64 `json:"addIntensity"`
FireEvent *Event `json:"fireEvent"`
SetPower *bool `json:"setPower"`
SetColor *string `json:"setColor"`
SetIntensity *float64 `json:"setIntensity"`
SetTemperature *int `json:"setTemperature"`
AddIntensity *float64 `json:"addIntensity"`
FireEvent *Event `json:"fireEvent"`
SetScene *DeviceSceneAssignment `json:"setScene"`
PushScene *DeviceSceneAssignment `json:"pushScene"`
} }
func (action *EventAction) Apply(other EventAction) { 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
models/scene.go

@ -114,7 +114,7 @@ func (r *SceneRole) Validate() error {
} }
switch r.PowerMode { switch r.PowerMode {
case SPScene, SPDevice:
case SPScene, SPDevice, SPBoth:
default: default:
return ErrSceneRoleUnknownPowerMode return ErrSceneRoleUnknownPowerMode
} }
@ -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
@ -226,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

6
scene-examples/evening.yaml

@ -48,8 +48,8 @@ roles:
relative: false relative: false
order: +name order: +name
states: states:
- color: 'hs:250,0.6'
intensity: 0.2
- color: 'hs:250,0.7'
intensity: 0.25
- effect: Static - effect: Static
power_mode: Device power_mode: Device
@ -60,4 +60,4 @@ roles:
order: +name order: +name
states: states:
- color: 'k:2000' - color: 'k:2000'
intensity: 0.25
intensity: 0.3

19
scene-examples/flash.yaml

@ -1,9 +1,26 @@
name: Flash name: Flash
interval: 0 interval: 0
roles: 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 - effect: Static
power_mode: Device power_mode: Device
target_kind: All target_kind: All
target_value: "" target_value: ""
states: states:
- intensity: 1
- intensity: 1
color: hs:0,0

23
scene-examples/late.yaml

@ -1,5 +1,5 @@
name: Late name: Late
interval: 5000
interval: 60000
roles: roles:
- effect: Random - effect: Random
power_mode: Device power_mode: Device
@ -11,11 +11,15 @@ roles:
states: states:
- color: 'hs:35,1' - color: 'hs:35,1'
intensity: 0.08 intensity: 0.08
- color: 'hs:25,1'
intensity: 0.10
- color: 'hs:35,1'
intensity: 0.085
- color: 'hs:35,1'
intensity: 0.09
- color: 'hs:35,1'
intensity: 0.12
- effect: Gradient - effect: Gradient
power_mode: Device
power_mode: Both
target_kind: Tag target_kind: Tag
target_value: Square target_value: Square
interpolate: true interpolate: true
@ -24,8 +28,13 @@ roles:
states: states:
- color: 'hs:25,1' - color: 'hs:25,1'
intensity: 0.05 intensity: 0.05
power: off
- color: 'hs:30,1'
intensity: 0.06
power: off
- color: 'hs:35,1' - color: 'hs:35,1'
intensity: 0.05
intensity: 0.07
power: on
- effect: Static - effect: Static
power_mode: Device power_mode: Device
@ -35,8 +44,8 @@ roles:
relative: false relative: false
order: +name order: +name
states: states:
- color: 'hs:25,1'
intensity: 0.05
- color: 'k:2000'
intensity: 0.15
- effect: Static - effect: Static
power_mode: Scene power_mode: Scene

4
scene-examples/morning.yaml

@ -9,10 +9,10 @@ roles:
relative: false relative: false
order: +name order: +name
states: states:
- color: 'hs:220,0.5'
intensity: 0.50
- color: 'hs:220,0.4' - color: 'hs:220,0.4'
intensity: 0.65 intensity: 0.65
- color: 'hs:220,0.5'
intensity: 0.50
- effect: Random - effect: Random
power_mode: Device power_mode: Device

42
scene-examples/saturday.yaml

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