3 Commits
Author SHA1 Message Date
gisle cedd0d1d28 add support for invoking and assigning scenes by name.
continuous-integration/drone/push Build is passing
2022-02-12 11:36:23 +01:00
gisle 7471102413 add scene list subcommand.
continuous-integration/drone/push Build is passing
2022-02-10 20:46:05 +01:00
gisle bf4d000455 add info mode to lucy list command. 2022-02-10 20:38:10 +01:00
9 changed files with 248 additions and 18 deletions
+21 -7
View File
@@ -218,14 +218,28 @@ func Devices(r gin.IRoutes) {
return []models.Device{}, nil
}
_, err = config.SceneRepository().Find(ctxOf(c), body.SceneID)
if err != nil {
return nil, err
var scene *models.Scene
if body.SceneName != "" {
scene, err = config.SceneRepository().FindName(ctxOf(c), body.SceneName)
if err != nil {
return nil, err
}
if body.DurationMS < 0 {
body.DurationMS = 0
}
body.StartTime = time.Now()
body.SceneID = scene.ID
} else {
scene, err = config.SceneRepository().Find(ctxOf(c), body.SceneID)
if err != nil {
return nil, err
}
if body.DurationMS < 0 {
body.DurationMS = 0
}
body.StartTime = time.Now()
body.SceneName = scene.Name
}
if body.DurationMS < 0 {
body.DurationMS = 0
}
body.StartTime = time.Now()
pushMode := c.Query("push") == "true"
for i := range devices {
+14 -2
View File
@@ -5,15 +5,27 @@ import (
"git.aiterp.net/lucifer/new-server/app/services/publisher"
"git.aiterp.net/lucifer/new-server/models"
"github.com/gin-gonic/gin"
"regexp"
)
func Scenes(r gin.IRoutes) {
nonNumericRegex := regexp.MustCompile("[a-zA-Z ]")
findScene := func(c *gin.Context) (*models.Scene, error) {
param := c.Param("id")
if nonNumericRegex.MatchString(param) {
return config.SceneRepository().FindName(ctxOf(c), param)
} else {
return config.SceneRepository().Find(ctxOf(c), intParam(c, "id"))
}
}
r.GET("", handler(func(c *gin.Context) (interface{}, error) {
return config.SceneRepository().FetchAll(ctxOf(c))
}))
r.GET("/:id", handler(func(c *gin.Context) (interface{}, error) {
return config.SceneRepository().Find(ctxOf(c), intParam(c, "id"))
return findScene(c)
}))
r.POST("", handler(func(c *gin.Context) (interface{}, error) {
@@ -45,7 +57,7 @@ func Scenes(r gin.IRoutes) {
return nil, err
}
scene, err := config.SceneRepository().Find(ctxOf(c), intParam(c, "id"))
scene, err := findScene(c)
if err != nil {
return nil, err
}
+157
View File
@@ -0,0 +1,157 @@
package main
import (
"encoding/json"
"fmt"
"git.aiterp.net/lucifer/new-server/models"
"io"
"sort"
"strings"
"time"
)
func WriteDeviceInfo(w io.Writer, devices []models.Device, fields []string) {
if len(fields) == 1 && fields[0] == "all" {
fields = allFields
} else {
fields = append(fields[:0:0], fields...)
for i, field := range fields {
if aliased, ok := fieldAliases[field]; ok {
fields[i] = aliased
}
}
}
length := 0
for _, field := range fields {
if mlFields[field] {
continue
}
if len(field) > length {
length = len(field)
}
}
for i, device := range devices {
if i > 0 {
fmt.Println()
}
for _, field := range fields {
if formatter, ok := fieldFormatters[field]; ok {
_, _ = fmt.Fprintf(w, "%-*s %s\n", length+1, field+":", formatter(&device))
} else {
_, _ = fmt.Fprintf(w, "%-*s (unknown field)\n", length+1, field+":")
}
}
}
}
var allFields = []string{
"id", "name", "icon", "bridgeId", "tags", "buttonNames",
"power", "color", "intensity", "temperature",
"internalId", "sceneAssignment", "userProperties", "driverProperties",
}
var mlFields = map[string]bool{
"sceneAssignment": true,
"userProperties": true,
"driverProperties": true,
}
var fieldFormatters = map[string]func(d *models.Device) string{
"id": func(d *models.Device) string { return fmt.Sprint(d.ID) },
"internalId": func(d *models.Device) string { return d.InternalID },
"name": func(d *models.Device) string { return d.Name },
"icon": func(d *models.Device) string { return d.Icon },
"bridgeId": func(d *models.Device) string { return fmt.Sprint(d.BridgeID) },
"tags": func(d *models.Device) string { return strings.Join(d.Tags, ", ") },
"buttonNames": func(d *models.Device) string { return strings.Join(d.ButtonNames, ", ") },
"power": func(d *models.Device) string { return fmt.Sprint(d.State.Power) },
"color": func(d *models.Device) string { return d.State.Color.String() },
"intensity": func(d *models.Device) string { return fmt.Sprint(d.State.Intensity) },
"temperature": func(d *models.Device) string { return fmt.Sprint(d.State.Temperature) },
"sceneAssignment": func(d *models.Device) string {
str := &strings.Builder{}
str.Grow(64)
for _, scene := range d.SceneAssignments {
_, _ = fmt.Fprintf(str, "\n - id: %d\n", scene.SceneID)
_, _ = fmt.Fprintf(str, " duration: %d\n", scene.DurationMS)
_, _ = fmt.Fprintf(str, " group: %s\n", scene.Group)
_, _ = fmt.Fprintf(str, " startTime: %s", scene.StartTime.Format(time.RFC3339))
}
if len(d.SceneAssignments) == 0 {
str.Reset()
str.WriteString("(none)")
}
return str.String()
},
"userProperties": func(d *models.Device) string {
str := &strings.Builder{}
str.Grow(64)
str.WriteString("\n")
keys := make([]string, 0, len(d.UserProperties))
for key := range d.UserProperties {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
_, _ = fmt.Fprintf(str, " %s: %s\n", key, d.UserProperties[key])
}
if len(d.UserProperties) == 0 {
str.Reset()
str.WriteString("(none)")
}
return str.String()
},
"driverProperties": func(d *models.Device) string {
str := &strings.Builder{}
str.Grow(256)
str.WriteString("\n")
keys := make([]string, 0, len(d.DriverProperties))
for key := range d.DriverProperties {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
j, _ := json.Marshal(d.DriverProperties[key])
_, _ = fmt.Fprintf(str, " %s: %s\n", key, string(j))
}
if len(d.DriverProperties) == 0 {
str.Reset()
str.WriteString("(none)")
}
return str.String()
},
"capabilities": func(d *models.Device) string {
capStr := make([]string, len(d.Capabilities))
for i := range d.Capabilities {
capStr[i] = string(d.Capabilities[i])
}
return strings.Join(capStr, ", ")
},
}
var fieldAliases = map[string]string{
"bridge": "bridgeId",
"buttons": "buttonNames",
"internal": "internalId",
"caps": "capabilities",
"cap": "capabilities",
"userprops": "userProperties",
"uprops": "userProperties",
"driverprops": "driverProperties",
"dprops": "driverProperties",
"internalid": "internalId",
"bridgeid": "bridgeId",
"buttonnames": "buttonNames",
"scene": "sceneAssignment",
}
+2 -2
View File
@@ -37,8 +37,8 @@ func main() {
log.Fatalln(err)
}
if cmd.Params.Get("info").StringOr("false") == "true" {
WriteDeviceInfoTable(os.Stdout, devices)
if info := cmd.Params.Get("info").String(); info != nil {
WriteDeviceInfo(os.Stdout, devices, strings.Split(*info, ","))
} else {
WriteDeviceStateTable(os.Stdout, devices)
}
+22 -5
View File
@@ -19,6 +19,16 @@ func sceneCmd(
cmd := parseCommand(os.Args[2:])
switch cmd.Name {
case "list":
{
scenes, err := c.GetScenes(ctx)
if err != nil {
log.Fatalln("Failed to load scenes:", err)
}
WriteSceneTable(os.Stdout, scenes)
}
case "create", "update":
{
fileName := cmd.Params.Get(0).String()
@@ -83,15 +93,22 @@ func sceneCmd(
{
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>")
name := cmd.Params.Get(1).String()
if fetch == nil || (id == nil && name == nil) {
log.Println("Usage: lucy scene assign <fetch> <id|name> <group=S> <duration=I>")
}
devices, err := c.AssignDevice(ctx, *fetch, cmd.Name == "push", models.DeviceSceneAssignment{
SceneID: *id,
assignment := models.DeviceSceneAssignment{
Group: cmd.Params.Get("group").StringOr(*fetch),
DurationMS: int64(cmd.Params.Get("duration").IntOr(0)),
})
}
if id != nil {
assignment.SceneID = *id
} else {
assignment.SceneName = *name
}
devices, err := c.AssignDevice(ctx, *fetch, cmd.Name == "push", assignment)
if err != nil {
log.Println("Could not assign devices:", err)
return
+18
View File
@@ -94,6 +94,24 @@ func WriteDeviceInfoTable(w io.Writer, devices []models.Device) {
table.Render()
}
func WriteSceneTable(w io.Writer, scenes []models.Scene) {
table := tablewriter.NewWriter(w)
table.SetHeader([]string{"ID", "NAME", "ROLES", "INTERVAL"})
table.SetReflowDuringAutoWrap(false)
for _, v := range scenes {
table.Append([]string{
fmt.Sprint(v.ID),
v.Name,
fmt.Sprint(len(v.Roles)),
fmt.Sprint(v.IntervalMS),
})
}
table.Render()
}
func WriteHandlerInfoTable(w io.Writer, handlers []models.EventHandler) {
table := tablewriter.NewWriter(w)
table.SetHeader([]string{"ID", "EVENT NAME", "PRIORITY", "CONDITIONS", "TARGET", "ACTION"})
+2 -2
View File
@@ -322,8 +322,8 @@ func (d *Driver) Run(ctx context.Context, bridge models.Bridge, ch chan<- models
return err
}
fastTicker := time.NewTicker(time.Second / 10)
slowTicker := time.NewTicker(time.Second / 3)
fastTicker := time.NewTicker(time.Second / 5)
slowTicker := time.NewTicker(time.Second / 2)
selectedTicker := fastTicker
ticksUntilRefresh := 0
ticksSinceChange := 0
+10
View File
@@ -29,6 +29,16 @@ func (r *SceneRepo) Find(ctx context.Context, id int) (*models.Scene, error) {
return r.populateOne(&scene)
}
func (r *SceneRepo) FindName(ctx context.Context, name string) (*models.Scene, error) {
var scene sceneRecord
err := r.DBX.GetContext(ctx, &scene, "SELECT * FROM scene WHERE name = ?", name)
if err != nil {
return nil, dbErr(err)
}
return r.populateOne(&scene)
}
func (r *SceneRepo) FetchAll(ctx context.Context) ([]models.Scene, error) {
scenes := make([]sceneRecord, 0, 8)
err := r.DBX.SelectContext(ctx, &scenes, "SELECT * FROM scene")
+2
View File
@@ -299,6 +299,7 @@ const (
// instance is created for the group.
type DeviceSceneAssignment struct {
SceneID int `json:"sceneId"`
SceneName string `json:"sceneName"`
Group string `json:"group"`
StartTime time.Time `json:"start"`
DurationMS int64 `json:"durationMs"`
@@ -306,6 +307,7 @@ type DeviceSceneAssignment struct {
type SceneRepository interface {
Find(ctx context.Context, id int) (*Scene, error)
FindName(ctx context.Context, name string) (*Scene, error)
FetchAll(ctx context.Context) ([]Scene, error)
Save(ctx context.Context, bridge *Scene) error
Delete(ctx context.Context, bridge *Scene) error