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 (
|
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,16 @@ func (client *Client) PutDeviceTags(ctx context.Context, fetchStr string, addTag
|
|||||||
return devices, nil
|
return devices, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (client *Client) AssignDevice(ctx context.Context, fetchStr string, assignment models.DeviceSceneAssignment) ([]models.Device, error) {
|
||||||
|
devices := make([]models.Device, 0, 16)
|
||||||
|
err := client.Fetch(ctx, "PUT", "/api/devices/"+fetchStr+"/scene", &devices, assignment)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return devices, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (client *Client) FireEvent(ctx context.Context, event models.Event) error {
|
func (client *Client) FireEvent(ctx context.Context, event models.Event) error {
|
||||||
err := client.Fetch(ctx, "POST", "/api/events", nil, event)
|
err := client.Fetch(ctx, "POST", "/api/events", nil, event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -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{}
|
waiting map[int]chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Publisher) UpdateScene(data models.Scene) {
|
||||||
|
p.mu.Lock()
|
||||||
|
p.sceneData[data.ID] = &data
|
||||||
|
|
||||||
|
for _, scene := range p.scenes {
|
||||||
|
if scene.data.ID == data.ID {
|
||||||
|
scene.UpdateScene(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Publisher) ReloadScenes(ctx context.Context) error {
|
func (p *Publisher) ReloadScenes(ctx context.Context) error {
|
||||||
scenes, err := config.SceneRepository().FetchAll(ctx)
|
scenes, err := config.SceneRepository().FetchAll(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -26,8 +38,12 @@ func (p *Publisher) ReloadScenes(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
for _, scene := range scenes {
|
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()
|
||||||
|
|
||||||
@@ -90,10 +106,12 @@ func (p *Publisher) Run() {
|
|||||||
|
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
for i, scene := range p.scenes {
|
for i, scene := range p.scenes {
|
||||||
if !scene.endTime.IsZero() && time.Now().After(scene.endTime) {
|
if (!scene.endTime.IsZero() && time.Now().After(scene.endTime)) || scene.Empty() {
|
||||||
deleteList = append(deleteList, i-len(deleteList))
|
deleteList = append(deleteList, i-len(deleteList))
|
||||||
updatedList = append(updatedList, scene.AllDevices()...)
|
updatedList = append(updatedList, scene.AllDevices()...)
|
||||||
|
|
||||||
|
log.Printf("Removing scene instance for %s (%d)", scene.data.Name, scene.data.ID)
|
||||||
|
|
||||||
for _, device := range scene.AllDevices() {
|
for _, device := range scene.AllDevices() {
|
||||||
p.sceneAssignment[device.ID] = nil
|
p.sceneAssignment[device.ID] = nil
|
||||||
}
|
}
|
||||||
@@ -258,6 +276,10 @@ var publisher = Publisher{
|
|||||||
waiting: make(map[int]chan struct{}),
|
waiting: make(map[int]chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Global() *Publisher {
|
||||||
|
return &publisher
|
||||||
|
}
|
||||||
|
|
||||||
func Initialize(ctx context.Context) error {
|
func Initialize(ctx context.Context) error {
|
||||||
err :=publisher.ReloadScenes(ctx)
|
err :=publisher.ReloadScenes(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -86,6 +86,17 @@ func (s *Scene) RemoveDevice(device models.Device) {
|
|||||||
delete(s.roleMap, device.ID)
|
delete(s.roleMap, device.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Scene) Empty() bool {
|
||||||
|
for _, list := range s.roleList {
|
||||||
|
if len(list) > 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func (s *Scene) Due() bool {
|
func (s *Scene) Due() bool {
|
||||||
if s.due {
|
if s.due {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -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,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()
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-4
@@ -56,7 +56,7 @@ func (s *Scene) RoleIndex(device *Device) int {
|
|||||||
type SceneEffect string
|
type SceneEffect string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SEStatic SceneEffect = "Plain"
|
SEStatic SceneEffect = "Static"
|
||||||
SERandom SceneEffect = "Random"
|
SERandom SceneEffect = "Random"
|
||||||
SEGradient SceneEffect = "Gradient"
|
SEGradient SceneEffect = "Gradient"
|
||||||
SEWalkingGradient SceneEffect = "WalkingGradient"
|
SEWalkingGradient SceneEffect = "WalkingGradient"
|
||||||
@@ -120,7 +120,7 @@ func (r *SceneRole) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch r.Effect {
|
switch r.Effect {
|
||||||
case SEStatic, SERandom, SEGradient, SETransition:
|
case SEStatic, SERandom, SEGradient, SEWalkingGradient, SETransition:
|
||||||
default:
|
default:
|
||||||
return ErrSceneRoleUnknownEffect
|
return ErrSceneRoleUnknownEffect
|
||||||
}
|
}
|
||||||
@@ -201,8 +201,6 @@ func (r *SceneRole) State(fac float64) NewDeviceState {
|
|||||||
return r.States[0]
|
return r.States[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if r.Interpolate {
|
if r.Interpolate {
|
||||||
if len(r.States) == 2 {
|
if len(r.States) == 2 {
|
||||||
return r.States[0].Interpolate(r.States[1], fac)
|
return r.States[0].Interpolate(r.States[1], fac)
|
||||||
|
|||||||
@@ -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