feature-scenesystem (#1)
continuous-integration/drone/push Build is passing

Co-authored-by: Gisle Aune <dev@gisle.me>
Reviewed-on: #1
Co-authored-by: gisle <gisle@hidden-email>
Co-committed-by: gisle <gisle@hidden-email>
This commit was merged in pull request #1.
This commit is contained in:
2021-10-06 18:13:16 +00:00
co-authored by gisle
parent d91bd472dd
commit ba22187e6e
38 changed files with 1898 additions and 225 deletions
+12
View File
@@ -5,6 +5,7 @@ import (
"log"
"strconv"
"strings"
"time"
)
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 {
Name string
+9 -1
View File
@@ -99,7 +99,7 @@ func handlerCmd(
func applyCmdToHandler(model models.EventHandler, cmd Command) models.EventHandler {
// Remove keys
for _, elem := range cmd.Params.Strings(1) {
if elem[0] != '-' {
if !strings.HasPrefix(elem, "-") {
continue
}
keyToRemove := elem[1:]
@@ -128,6 +128,10 @@ func applyCmdToHandler(model models.EventHandler, cmd Command) models.EventHandl
model.From = models.Never
case "to":
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
}
+3
View File
@@ -96,6 +96,9 @@ func main() {
case "handler":
handlerCmd(ctx, c)
case "scene":
sceneCmd(ctx, c)
// EVENT
case "run":
{
+165
View File
@@ -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
View File
@@ -14,16 +14,32 @@ func WriteDeviceStateTable(w io.Writer, devices []models.Device) {
table.SetHeader([]string{"ID", "NAME", "POWER", "COLOR", "INTENSITY", "TEMPERATURE"})
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 {
powerStr := ""
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 := ""
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 := ""
@@ -33,7 +49,11 @@ func WriteDeviceStateTable(w io.Writer, devices []models.Device) {
intensityString := ""
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{
@@ -104,6 +124,17 @@ func WriteHandlerInfoTable(w io.Writer, handlers []models.EventHandler) {
if h.Actions.FireEvent != nil {
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
if h.OneShot {