scripting system, hue fixes and such
This commit is contained in:
@@ -13,10 +13,9 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewService(resolver device.Resolver, sceneMap device.SceneMap) lucifer3.ActiveService {
|
||||
func NewService(resolver device.Resolver) lucifer3.ActiveService {
|
||||
s := &effectEnforcer{
|
||||
resolver: resolver,
|
||||
sceneMap: sceneMap,
|
||||
list: make([]*effectEnforcerRun, 0, 16),
|
||||
index: make(map[string]*effectEnforcerRun, 8),
|
||||
|
||||
@@ -32,7 +31,6 @@ func NewService(resolver device.Resolver, sceneMap device.SceneMap) lucifer3.Act
|
||||
type effectEnforcer struct {
|
||||
mu sync.Mutex
|
||||
resolver device.Resolver
|
||||
sceneMap device.SceneMap
|
||||
|
||||
supportFlags map[string]device.SupportFlags
|
||||
colorFlags map[string]device.ColorFlags
|
||||
@@ -68,8 +66,7 @@ func (s *effectEnforcer) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Even
|
||||
s.mu.Unlock()
|
||||
|
||||
case events.DeviceReady:
|
||||
// If the device is managed by the effect enforcer, cause the effect to be
|
||||
// re-ran.
|
||||
// If the device is managed by the effect enforcer, cause the effect to run again
|
||||
s.mu.Lock()
|
||||
if run, ok := s.index[event.ID]; ok {
|
||||
run.due = time.Now()
|
||||
@@ -98,11 +95,9 @@ func (s *effectEnforcer) HandleCommand(bus *lucifer3.EventBus, command lucifer3.
|
||||
switch command := command.(type) {
|
||||
case commands.Assign:
|
||||
pointers := s.resolver.Resolve(command.Match)
|
||||
allowedIDs := make([]string, 0, len(pointers))
|
||||
deviceIDs := make([]string, 0, len(pointers))
|
||||
for _, ptr := range pointers {
|
||||
if s.sceneMap.SceneID(ptr.ID) == nil {
|
||||
allowedIDs = append(allowedIDs, ptr.ID)
|
||||
}
|
||||
deviceIDs = append(deviceIDs, ptr.ID)
|
||||
}
|
||||
if len(pointers) == 0 {
|
||||
if command.ID != nil {
|
||||
@@ -135,13 +130,13 @@ func (s *effectEnforcer) HandleCommand(bus *lucifer3.EventBus, command lucifer3.
|
||||
match: command.Match,
|
||||
id: id,
|
||||
due: time.Now(),
|
||||
ids: allowedIDs,
|
||||
ids: deviceIDs,
|
||||
effect: command.Effect,
|
||||
}
|
||||
s.list = append(s.list, newRun)
|
||||
|
||||
// Switch over the indices.
|
||||
for _, id := range allowedIDs {
|
||||
for _, id := range deviceIDs {
|
||||
if oldRun := s.index[id]; oldRun != nil {
|
||||
oldRun.remove(id)
|
||||
}
|
||||
@@ -153,7 +148,7 @@ func (s *effectEnforcer) HandleCommand(bus *lucifer3.EventBus, command lucifer3.
|
||||
go s.runLoop(bus)
|
||||
}
|
||||
|
||||
for _, deviceID := range allowedIDs {
|
||||
for _, deviceID := range deviceIDs {
|
||||
bus.RunEvent(events.DeviceAssigned{
|
||||
DeviceID: deviceID,
|
||||
AssignmentID: gentools.Ptr(id),
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"git.aiterp.net/lucifer3/server/commands"
|
||||
"git.aiterp.net/lucifer3/server/effects"
|
||||
"git.aiterp.net/lucifer3/server/events"
|
||||
"git.aiterp.net/lucifer3/server/services/script"
|
||||
"git.aiterp.net/lucifer3/server/services/uistate"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
@@ -52,6 +53,8 @@ func New(addr string) (lucifer3.Service, error) {
|
||||
})
|
||||
case input.PairDevice != nil:
|
||||
bus.RunCommand(*input.PairDevice)
|
||||
case input.ConnectDevice != nil:
|
||||
bus.RunCommand(*input.ConnectDevice)
|
||||
case input.ForgetDevice != nil:
|
||||
bus.RunCommand(*input.ForgetDevice)
|
||||
case input.SearchDevices != nil:
|
||||
@@ -60,6 +63,10 @@ func New(addr string) (lucifer3.Service, error) {
|
||||
bus.RunCommand(*input.AddAlias)
|
||||
case input.RemoveAlias != nil:
|
||||
bus.RunCommand(*input.RemoveAlias)
|
||||
case input.UpdateScript != nil:
|
||||
bus.RunCommand(*input.UpdateScript)
|
||||
case input.ExecuteScript != nil:
|
||||
bus.RunCommand(*input.ExecuteScript)
|
||||
default:
|
||||
return c.String(400, "No supported command found in input")
|
||||
}
|
||||
@@ -113,8 +120,11 @@ type commandInput struct {
|
||||
AddAlias *commands.AddAlias `json:"addAlias,omitempty"`
|
||||
RemoveAlias *commands.RemoveAlias `json:"removeAlias,omitempty"`
|
||||
PairDevice *commands.PairDevice `json:"pairDevice,omitempty"`
|
||||
ConnectDevice *commands.ConnectDevice `json:"connectDevice,omitempty"`
|
||||
SearchDevices *commands.SearchDevices `json:"searchDevices,omitempty"`
|
||||
ForgetDevice *commands.ForgetDevice `json:"forgetDevice,omitempty"`
|
||||
UpdateScript *script.UpdateScript `json:"updateScript,omitempty"`
|
||||
ExecuteScript *script.ExecuteScript `json:"executeScript,omitempty"`
|
||||
}
|
||||
|
||||
type assignInput struct {
|
||||
|
||||
+3
-20
@@ -7,7 +7,6 @@ import (
|
||||
lucifer3 "git.aiterp.net/lucifer3/server"
|
||||
"git.aiterp.net/lucifer3/server/device"
|
||||
"git.aiterp.net/lucifer3/server/events"
|
||||
"git.aiterp.net/lucifer3/server/internal/color"
|
||||
"git.aiterp.net/lucifer3/server/internal/gentools"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"log"
|
||||
@@ -412,6 +411,9 @@ func (b *Bridge) Run(ctx context.Context, bus *lucifer3.EventBus) interface{} {
|
||||
|
||||
func (b *Bridge) makeCongruentLoop(ctx context.Context) {
|
||||
for range b.triggerCongruenceCheckCh {
|
||||
// I hate that I have to do this, but the SSE is not being reliable.
|
||||
_, _ = b.RefreshAll()
|
||||
|
||||
if ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
@@ -428,8 +430,6 @@ func (b *Bridge) makeCongruentLoop(ctx context.Context) {
|
||||
colorFlags := b.colorFlags
|
||||
b.mu.Unlock()
|
||||
|
||||
newActiveStates := make(map[string]device.State, 0)
|
||||
|
||||
updates := make(map[string]ResourceUpdate)
|
||||
for id, desired := range desiredStates {
|
||||
active, activeOK := activeStates[id]
|
||||
@@ -452,7 +452,6 @@ func (b *Bridge) makeCongruentLoop(ctx context.Context) {
|
||||
|
||||
newActiveState := activeStates[id]
|
||||
newActiveState.Power = gentools.Ptr(*desired.Power)
|
||||
newActiveStates[id] = newActiveState
|
||||
|
||||
continue
|
||||
}
|
||||
@@ -463,7 +462,6 @@ func (b *Bridge) makeCongruentLoop(ctx context.Context) {
|
||||
|
||||
updated := false
|
||||
update := ResourceUpdate{}
|
||||
newActiveState := activeStates[id]
|
||||
|
||||
if active.Color != nil && desired.Color != nil {
|
||||
ac := *active.Color
|
||||
@@ -497,8 +495,6 @@ func (b *Bridge) makeCongruentLoop(ctx context.Context) {
|
||||
acMirek = 1000000 / *ac.K
|
||||
}
|
||||
if acMirek != dcMirek {
|
||||
newActiveState.Color = &color.Color{K: gentools.Ptr(*dc.K)}
|
||||
|
||||
update.Mirek = &dcMirek
|
||||
updated = true
|
||||
}
|
||||
@@ -508,7 +504,6 @@ func (b *Bridge) makeCongruentLoop(ctx context.Context) {
|
||||
if active.Intensity != nil && desired.Intensity != nil {
|
||||
if math.Abs(*active.Intensity-*desired.Intensity) >= 0.01 {
|
||||
update.Brightness = gentools.Ptr(*desired.Intensity * 100)
|
||||
newActiveState.Intensity = gentools.Ptr(*desired.Intensity)
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
@@ -516,7 +511,6 @@ func (b *Bridge) makeCongruentLoop(ctx context.Context) {
|
||||
if updated {
|
||||
update.TransitionDuration = gentools.Ptr(time.Millisecond * 101)
|
||||
updates["light/"+*lightID] = update
|
||||
newActiveStates[id] = newActiveState
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,17 +533,6 @@ func (b *Bridge) makeCongruentLoop(ctx context.Context) {
|
||||
log.Println("Failed to run update", err)
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
activeStates = b.activeStates
|
||||
b.mu.Unlock()
|
||||
activeStates = gentools.CopyMap(activeStates)
|
||||
for id, state := range newActiveStates {
|
||||
activeStates[id] = state
|
||||
}
|
||||
b.mu.Lock()
|
||||
b.activeStates = activeStates
|
||||
b.mu.Unlock()
|
||||
|
||||
cancel()
|
||||
|
||||
// Wait the remaining time for the rate limit
|
||||
|
||||
@@ -205,6 +205,22 @@ func (res *ResourceData) GenerateEvent(hostname string, resources map[string]*Re
|
||||
}
|
||||
buttonCount := 0
|
||||
|
||||
switch res.ProductData.ProductArchetype {
|
||||
case "candle_bulb":
|
||||
hwMeta.Icon = "hue_lightbulb_e14"
|
||||
case "sultan_bulb":
|
||||
hwMeta.Icon = "hue_lightbulb_e27"
|
||||
case "hue_signe":
|
||||
hwMeta.Icon = "hue_signe"
|
||||
case "unknown_archetype":
|
||||
switch res.ProductData.ProductName {
|
||||
case "Hue motion sensor":
|
||||
hwMeta.Icon = "hue_motionsensor"
|
||||
case "Hue dimmer switch":
|
||||
hwMeta.Icon = "hue_dimmerswitch"
|
||||
}
|
||||
}
|
||||
|
||||
if res.ProductData != nil {
|
||||
hwMeta.FirmwareVersion = res.ProductData.SoftwareVersion
|
||||
}
|
||||
@@ -365,6 +381,27 @@ type ResourceUpdate struct {
|
||||
TransitionDuration *time.Duration
|
||||
}
|
||||
|
||||
func (r ResourceUpdate) Split() []ResourceUpdate {
|
||||
updates := make([]ResourceUpdate, 0, 2)
|
||||
if r.Name != nil {
|
||||
updates = append(updates, ResourceUpdate{Name: r.Name, TransitionDuration: r.TransitionDuration})
|
||||
}
|
||||
if r.Power != nil {
|
||||
updates = append(updates, ResourceUpdate{Power: r.Power, TransitionDuration: r.TransitionDuration})
|
||||
}
|
||||
if r.ColorXY != nil {
|
||||
updates = append(updates, ResourceUpdate{ColorXY: r.ColorXY, TransitionDuration: r.TransitionDuration})
|
||||
}
|
||||
if r.Brightness != nil {
|
||||
updates = append(updates, ResourceUpdate{Brightness: r.Brightness, TransitionDuration: r.TransitionDuration})
|
||||
}
|
||||
if r.Mirek != nil {
|
||||
updates = append(updates, ResourceUpdate{Mirek: r.Mirek, TransitionDuration: r.TransitionDuration})
|
||||
}
|
||||
|
||||
return updates
|
||||
}
|
||||
|
||||
func (r ResourceUpdate) MarshalJSON() ([]byte, error) {
|
||||
chunks := make([]string, 0, 4)
|
||||
if r.Name != nil {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE script (
|
||||
name VARCHAR(255) NOT NULL PRIMARY KEY,
|
||||
data JSON NOT NULL
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE IF EXISTS script;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,15 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE script_variable (
|
||||
scope VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
|
||||
PRIMARY KEY (scope, name)
|
||||
)
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE IF EXISTS script_variable;
|
||||
-- +goose StatementEnd
|
||||
@@ -63,6 +63,12 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
|
||||
if q.listDeviceInfosStmt, err = db.PrepareContext(ctx, listDeviceInfos); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query ListDeviceInfos: %w", err)
|
||||
}
|
||||
if q.listScriptVariablesStmt, err = db.PrepareContext(ctx, listScriptVariables); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query ListScriptVariables: %w", err)
|
||||
}
|
||||
if q.listScriptsStmt, err = db.PrepareContext(ctx, listScripts); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query ListScripts: %w", err)
|
||||
}
|
||||
if q.replaceDeviceAssignmentStmt, err = db.PrepareContext(ctx, replaceDeviceAssignment); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query ReplaceDeviceAssignment: %w", err)
|
||||
}
|
||||
@@ -72,6 +78,12 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
|
||||
if q.replaceDeviceInfoStmt, err = db.PrepareContext(ctx, replaceDeviceInfo); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query ReplaceDeviceInfo: %w", err)
|
||||
}
|
||||
if q.saveScriptStmt, err = db.PrepareContext(ctx, saveScript); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query SaveScript: %w", err)
|
||||
}
|
||||
if q.updateScriptVariablesStmt, err = db.PrepareContext(ctx, updateScriptVariables); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query UpdateScriptVariables: %w", err)
|
||||
}
|
||||
return &q, nil
|
||||
}
|
||||
|
||||
@@ -142,6 +154,16 @@ func (q *Queries) Close() error {
|
||||
err = fmt.Errorf("error closing listDeviceInfosStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.listScriptVariablesStmt != nil {
|
||||
if cerr := q.listScriptVariablesStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing listScriptVariablesStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.listScriptsStmt != nil {
|
||||
if cerr := q.listScriptsStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing listScriptsStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.replaceDeviceAssignmentStmt != nil {
|
||||
if cerr := q.replaceDeviceAssignmentStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing replaceDeviceAssignmentStmt: %w", cerr)
|
||||
@@ -157,6 +179,16 @@ func (q *Queries) Close() error {
|
||||
err = fmt.Errorf("error closing replaceDeviceInfoStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.saveScriptStmt != nil {
|
||||
if cerr := q.saveScriptStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing saveScriptStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.updateScriptVariablesStmt != nil {
|
||||
if cerr := q.updateScriptVariablesStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing updateScriptVariablesStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -209,9 +241,13 @@ type Queries struct {
|
||||
listDeviceAssignmentsStmt *sql.Stmt
|
||||
listDeviceAuthStmt *sql.Stmt
|
||||
listDeviceInfosStmt *sql.Stmt
|
||||
listScriptVariablesStmt *sql.Stmt
|
||||
listScriptsStmt *sql.Stmt
|
||||
replaceDeviceAssignmentStmt *sql.Stmt
|
||||
replaceDeviceAuthStmt *sql.Stmt
|
||||
replaceDeviceInfoStmt *sql.Stmt
|
||||
saveScriptStmt *sql.Stmt
|
||||
updateScriptVariablesStmt *sql.Stmt
|
||||
}
|
||||
|
||||
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
||||
@@ -231,8 +267,12 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
||||
listDeviceAssignmentsStmt: q.listDeviceAssignmentsStmt,
|
||||
listDeviceAuthStmt: q.listDeviceAuthStmt,
|
||||
listDeviceInfosStmt: q.listDeviceInfosStmt,
|
||||
listScriptVariablesStmt: q.listScriptVariablesStmt,
|
||||
listScriptsStmt: q.listScriptsStmt,
|
||||
replaceDeviceAssignmentStmt: q.replaceDeviceAssignmentStmt,
|
||||
replaceDeviceAuthStmt: q.replaceDeviceAuthStmt,
|
||||
replaceDeviceInfoStmt: q.replaceDeviceInfoStmt,
|
||||
saveScriptStmt: q.saveScriptStmt,
|
||||
updateScriptVariablesStmt: q.updateScriptVariablesStmt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,3 +34,14 @@ type DeviceInfo struct {
|
||||
Kind string
|
||||
Data json.RawMessage
|
||||
}
|
||||
|
||||
type Script struct {
|
||||
Name string
|
||||
Data json.RawMessage
|
||||
}
|
||||
|
||||
type ScriptVariable struct {
|
||||
Scope string
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.13.0
|
||||
// source: script.sql
|
||||
|
||||
package mysqlgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
const listScriptVariables = `-- name: ListScriptVariables :many
|
||||
SELECT scope, name, value FROM script_variable
|
||||
`
|
||||
|
||||
func (q *Queries) ListScriptVariables(ctx context.Context) ([]ScriptVariable, error) {
|
||||
rows, err := q.query(ctx, q.listScriptVariablesStmt, listScriptVariables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []ScriptVariable{}
|
||||
for rows.Next() {
|
||||
var i ScriptVariable
|
||||
if err := rows.Scan(&i.Scope, &i.Name, &i.Value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listScripts = `-- name: ListScripts :many
|
||||
SELECT name, data FROM script
|
||||
`
|
||||
|
||||
func (q *Queries) ListScripts(ctx context.Context) ([]Script, error) {
|
||||
rows, err := q.query(ctx, q.listScriptsStmt, listScripts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []Script{}
|
||||
for rows.Next() {
|
||||
var i Script
|
||||
if err := rows.Scan(&i.Name, &i.Data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const saveScript = `-- name: SaveScript :exec
|
||||
REPLACE INTO script (name, data) VALUES (?, ?)
|
||||
`
|
||||
|
||||
type SaveScriptParams struct {
|
||||
Name string
|
||||
Data json.RawMessage
|
||||
}
|
||||
|
||||
func (q *Queries) SaveScript(ctx context.Context, arg SaveScriptParams) error {
|
||||
_, err := q.exec(ctx, q.saveScriptStmt, saveScript, arg.Name, arg.Data)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateScriptVariables = `-- name: UpdateScriptVariables :exec
|
||||
REPLACE INTO script_variable (scope, name, value) VALUES (?, ?, ?)
|
||||
`
|
||||
|
||||
type UpdateScriptVariablesParams struct {
|
||||
Scope string
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateScriptVariables(ctx context.Context, arg UpdateScriptVariablesParams) error {
|
||||
_, err := q.exec(ctx, q.updateScriptVariablesStmt, updateScriptVariables, arg.Scope, arg.Name, arg.Value)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
-- name: ListScripts :many
|
||||
SELECT * FROM script;
|
||||
|
||||
-- name: SaveScript :exec
|
||||
REPLACE INTO script (name, data) VALUES (?, ?);
|
||||
|
||||
-- name: ListScriptVariables :many
|
||||
SELECT * FROM script_variable;
|
||||
|
||||
-- name: UpdateScriptVariables :exec
|
||||
REPLACE INTO script_variable (scope, name, value) VALUES (?, ?, ?);
|
||||
@@ -11,13 +11,16 @@ import (
|
||||
"git.aiterp.net/lucifer3/server/events"
|
||||
"git.aiterp.net/lucifer3/server/internal/gentools"
|
||||
"git.aiterp.net/lucifer3/server/services/mysqldb/mysqlgen"
|
||||
"git.aiterp.net/lucifer3/server/services/script"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
type database struct {
|
||||
db *sql.DB
|
||||
db *sql.DB
|
||||
scriptVariables map[string]string
|
||||
}
|
||||
|
||||
func (d *database) Active() bool {
|
||||
@@ -26,7 +29,7 @@ func (d *database) Active() bool {
|
||||
|
||||
func (d *database) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Event) {
|
||||
q := mysqlgen.New(d.db)
|
||||
timeout, cancel := context.WithTimeout(context.Background(), time.Second/2)
|
||||
timeout, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
|
||||
switch event := event.(type) {
|
||||
@@ -119,6 +122,58 @@ func (d *database) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Event) {
|
||||
})
|
||||
}
|
||||
|
||||
scripts, err := q.ListScripts(timeout)
|
||||
if err != nil {
|
||||
bus.RunEvent(events.Log{
|
||||
Level: "error",
|
||||
Code: "database_could_not_list_scripts",
|
||||
Message: "Database could not list scripts: " + err.Error(),
|
||||
})
|
||||
}
|
||||
for _, scr := range scripts {
|
||||
var lines []script.Line
|
||||
err := json.Unmarshal(scr.Data, &lines)
|
||||
if err != nil {
|
||||
bus.RunEvent(events.Log{
|
||||
Level: "error",
|
||||
Code: "database_could_not_read_script",
|
||||
Message: "Database could not read script: " + err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
bus.RunCommand(script.UpdateScript{
|
||||
Name: scr.Name,
|
||||
Lines: lines,
|
||||
})
|
||||
}
|
||||
|
||||
scriptVariables, err := q.ListScriptVariables(timeout)
|
||||
if err != nil {
|
||||
bus.RunEvent(events.Log{
|
||||
Level: "error",
|
||||
Code: "database_could_not_list_script_variables",
|
||||
Message: "Database could not list script variables: " + err.Error(),
|
||||
})
|
||||
}
|
||||
for _, variable := range scriptVariables {
|
||||
d.scriptVariables[variable.Scope+"::"+variable.Name] = variable.Value
|
||||
|
||||
cmd := script.SetVariable{
|
||||
Key: variable.Name,
|
||||
Value: variable.Value,
|
||||
}
|
||||
|
||||
scopeKind, scopeValue, _ := strings.Cut(variable.Scope, ":")
|
||||
switch scopeKind {
|
||||
case "devices":
|
||||
cmd.Devices = strings.Split(scopeValue, ";")
|
||||
case "match":
|
||||
cmd.Match = gentools.Ptr(scopeValue)
|
||||
}
|
||||
|
||||
bus.RunCommand(cmd)
|
||||
}
|
||||
|
||||
case events.DeviceAccepted:
|
||||
if event.Extras == nil {
|
||||
event.Extras = map[string]string{}
|
||||
@@ -298,7 +353,43 @@ func (d *database) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Event) {
|
||||
}
|
||||
}
|
||||
|
||||
func (d *database) HandleCommand(*lucifer3.EventBus, lucifer3.Command) {}
|
||||
func (d *database) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command) {
|
||||
q := mysqlgen.New(d.db)
|
||||
timeout, cancel := context.WithTimeout(context.Background(), time.Second/2)
|
||||
defer cancel()
|
||||
|
||||
switch command := command.(type) {
|
||||
case script.SetVariable:
|
||||
scopeName := "global"
|
||||
if command.Match != nil {
|
||||
scopeName = "match:" + *command.Match
|
||||
} else if command.Devices != nil {
|
||||
scopeName = "devices:" + strings.Join(command.Devices, ";")
|
||||
}
|
||||
|
||||
if d.scriptVariables[scopeName+"::"+command.Key] != command.Value {
|
||||
_ = q.UpdateScriptVariables(timeout, mysqlgen.UpdateScriptVariablesParams{
|
||||
Scope: scopeName,
|
||||
Name: command.Key,
|
||||
Value: command.Value,
|
||||
})
|
||||
}
|
||||
case script.UpdateScript:
|
||||
j, _ := json.Marshal(command.Lines)
|
||||
|
||||
err := q.SaveScript(timeout, mysqlgen.SaveScriptParams{
|
||||
Name: command.Name,
|
||||
Data: j,
|
||||
})
|
||||
if err != nil {
|
||||
bus.RunEvent(events.Log{
|
||||
Level: "error",
|
||||
Code: "database_could_not_save_script",
|
||||
Message: "Failed to save script: " + err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Connect(host string, port int, username, password, dbname string) (lucifer3.ActiveService, error) {
|
||||
db, err := sql.Open("mysql", fmt.Sprintf(
|
||||
@@ -317,5 +408,5 @@ func Connect(host string, port int, username, password, dbname string) (lucifer3
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &database{db: db}, nil
|
||||
return &database{db: db, scriptVariables: make(map[string]string)}, nil
|
||||
}
|
||||
|
||||
@@ -154,6 +154,13 @@ func (r *Resolver) ensure(id string) *device.Pointer {
|
||||
return r.pointers[len(r.pointers)-1]
|
||||
}
|
||||
|
||||
func (r *Resolver) CompileMatcher(pattern string) device.PointerMatcher {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
return r.patternToGlob(pattern)
|
||||
}
|
||||
|
||||
func (r *Resolver) patternToGlob(pattern string) glob.Glob {
|
||||
if cached, ok := r.patternCache[pattern]; ok {
|
||||
return cached
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package script
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SetVariable struct {
|
||||
Match *string
|
||||
Devices []string
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
func (c SetVariable) CommandDescription() string {
|
||||
switch {
|
||||
case c.Match != nil:
|
||||
return fmt.Sprintf("SetVariable(tag(%s), %s, %s)", *c.Match, c.Key, c.Value)
|
||||
case c.Devices != nil:
|
||||
return fmt.Sprintf("SetVariable(devices(%s), %s, %s)", strings.Join(c.Devices, ", "), c.Key, c.Value)
|
||||
default:
|
||||
return fmt.Sprintf("SetVariable(global, %s, %s)", c.Key, c.Value)
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateScript struct {
|
||||
Name string `json:"name"`
|
||||
Lines []Line `json:"lines"`
|
||||
}
|
||||
|
||||
func (c UpdateScript) CommandDescription() string {
|
||||
return fmt.Sprintf("UpdateScript(%s, [%d lines...])", c.Name, len(c.Lines))
|
||||
}
|
||||
|
||||
type ExecuteScript struct {
|
||||
Name string `json:"name"`
|
||||
Match string `json:"match"`
|
||||
}
|
||||
|
||||
func (c ExecuteScript) CommandDescription() string {
|
||||
return fmt.Sprintf("ExecuteScript(%s, %s)", c.Name, c.Match)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package script
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Condition struct {
|
||||
Scope string `json:"scope"`
|
||||
Key string `json:"key"`
|
||||
Op string `json:"op"`
|
||||
Value string `json:"value,omitempty"`
|
||||
Not bool `json:"not,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Condition) Check(set VariableSet, match string, devices []string) bool {
|
||||
var value string
|
||||
switch c.Scope {
|
||||
case "global":
|
||||
value = set.Global(c.Key)
|
||||
case "match":
|
||||
value = set.Match(match, c.Key)
|
||||
case "devices":
|
||||
value = set.Devices(devices, c.Key)
|
||||
default:
|
||||
return c.Not
|
||||
}
|
||||
|
||||
valueInt, valueIntErr := strconv.ParseInt(value, 10, 64)
|
||||
targetInt, _ := strconv.ParseInt(c.Value, 10, 64)
|
||||
|
||||
switch c.Op {
|
||||
case "eq":
|
||||
return !c.Not == (value == c.Value)
|
||||
case "neq":
|
||||
return !c.Not == (value != c.Value)
|
||||
case "gt":
|
||||
return !c.Not == (valueInt > targetInt)
|
||||
case "lt":
|
||||
return !c.Not == (valueInt < targetInt)
|
||||
case "gte":
|
||||
log.Println(c.Not, valueInt, targetInt)
|
||||
return !c.Not == (valueInt >= targetInt)
|
||||
case "lte":
|
||||
return !c.Not == (valueInt <= targetInt)
|
||||
case "exists":
|
||||
return !c.Not == (value != "")
|
||||
case "contains":
|
||||
return !c.Not == strings.Contains(value, c.Value)
|
||||
case "numerical":
|
||||
return !c.Not == (valueIntErr == nil)
|
||||
default:
|
||||
return c.Not
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package script
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCondition_Check(t *testing.T) {
|
||||
variables := VariableSet{
|
||||
"global": map[string]string{
|
||||
"test": "32",
|
||||
},
|
||||
"devices:dummy:01;dummy:02": map[string]string{
|
||||
"tstxo": "tsyeym",
|
||||
},
|
||||
"match:lucifer:tag:Soverom": map[string]string{
|
||||
"mode": "daytime",
|
||||
"stuff": "54",
|
||||
},
|
||||
}
|
||||
|
||||
table := []struct {
|
||||
L string
|
||||
R bool
|
||||
C Condition
|
||||
M string
|
||||
D []string
|
||||
}{
|
||||
{
|
||||
"global_gte", true,
|
||||
Condition{"global", "test", "gte", "16", false},
|
||||
"", []string{},
|
||||
},
|
||||
{
|
||||
"global_!gte", true,
|
||||
Condition{"global", "test", "gte", "64", true},
|
||||
"", []string{},
|
||||
},
|
||||
{
|
||||
"global_eq", true,
|
||||
Condition{"global", "test", "eq", "32", false},
|
||||
"", []string{},
|
||||
},
|
||||
{
|
||||
"match_exists", true,
|
||||
Condition{"devices", "tstxo", "exists", "", false},
|
||||
"", []string{"dummy:01", "dummy:02"},
|
||||
},
|
||||
{
|
||||
"match_!exists", false,
|
||||
Condition{"devices", "tstxo", "exists", "", true},
|
||||
"", []string{"dummy:01", "dummy:02"},
|
||||
},
|
||||
{
|
||||
"devices_contains", true,
|
||||
Condition{"match", "mode", "contains", "time", false},
|
||||
"lucifer:tag:Soverom", []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, row := range table {
|
||||
t.Run(row.L, func(t *testing.T) {
|
||||
assert.Equal(t, row.R, row.C.Check(variables, row.M, row.D))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package script
|
||||
|
||||
import (
|
||||
"git.aiterp.net/lucifer3/server/effects"
|
||||
)
|
||||
|
||||
type Script struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Lines []Line `json:"lines,omitempty"`
|
||||
}
|
||||
|
||||
type Line struct {
|
||||
If *LineIf `json:"if,omitempty"`
|
||||
Assign *LineAssign `json:"assign,omitempty"`
|
||||
Set *LineSet `json:"set,omitempty"`
|
||||
}
|
||||
|
||||
type LineSet struct {
|
||||
Scope string `json:"scope"`
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type LineAssign struct {
|
||||
Match string `json:"match,omitempty"`
|
||||
Effect effects.Serializable `json:"effect"`
|
||||
}
|
||||
|
||||
type LineIf struct {
|
||||
Condition *Condition `json:"condition,omitempty"`
|
||||
Then []Line `json:"then,omitempty"`
|
||||
Else []Line `json:"else,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package script
|
||||
|
||||
import (
|
||||
lucifer3 "git.aiterp.net/lucifer3/server"
|
||||
"git.aiterp.net/lucifer3/server/commands"
|
||||
"git.aiterp.net/lucifer3/server/device"
|
||||
"git.aiterp.net/lucifer3/server/internal/formattools"
|
||||
"git.aiterp.net/lucifer3/server/internal/gentools"
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type service struct {
|
||||
mu sync.Mutex
|
||||
resolver device.Resolver
|
||||
variables *Variables
|
||||
scripts map[string]*Script
|
||||
}
|
||||
|
||||
func (s *service) Active() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *service) HandleEvent(_ *lucifer3.EventBus, _ lucifer3.Event) {}
|
||||
|
||||
func (s *service) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command) {
|
||||
switch command := command.(type) {
|
||||
case UpdateScript:
|
||||
s.mu.Lock()
|
||||
s.scripts[command.Name] = &Script{
|
||||
Name: command.Name,
|
||||
Lines: command.Lines,
|
||||
}
|
||||
s.mu.Unlock()
|
||||
case ExecuteScript:
|
||||
s.mu.Lock()
|
||||
script := s.scripts[command.Name]
|
||||
s.mu.Unlock()
|
||||
if script == nil {
|
||||
break
|
||||
}
|
||||
|
||||
variables := s.variables.Get()
|
||||
|
||||
devices := s.resolver.Resolve(command.Match)
|
||||
sort.Slice(devices, func(i, j int) bool {
|
||||
return devices[i].ID < devices[j].ID
|
||||
})
|
||||
|
||||
s.runScript(bus, script.Lines, command.Match, map[string]bool{}, devices, variables)
|
||||
}
|
||||
}
|
||||
|
||||
func NewService(resolver device.Resolver, variables *Variables) lucifer3.Service {
|
||||
return &service{
|
||||
resolver: resolver,
|
||||
variables: variables,
|
||||
scripts: map[string]*Script{},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) runScript(bus *lucifer3.EventBus, lines []Line, match string, matchedDevices map[string]bool, devices []device.Pointer, variables VariableSet) {
|
||||
for _, line := range lines {
|
||||
if line.If != nil {
|
||||
devicesIDs := gentools.Map(devices, func(d device.Pointer) string { return d.ID })
|
||||
if line.If.Condition.Check(variables, match, devicesIDs) {
|
||||
s.runScript(bus, line.If.Then, match, matchedDevices, devices, variables)
|
||||
} else {
|
||||
s.runScript(bus, line.If.Else, match, matchedDevices, devices, variables)
|
||||
}
|
||||
} else if line.Assign != nil {
|
||||
matcher := s.resolver.CompileMatcher(line.Assign.Match)
|
||||
|
||||
matched := make([]string, 0)
|
||||
for _, dev := range devices {
|
||||
if matchedDevices[dev.ID] {
|
||||
continue
|
||||
}
|
||||
|
||||
if dev.Matches(matcher) {
|
||||
matchedDevices[dev.ID] = true
|
||||
matched = append(matched, dev.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(matched) > 0 {
|
||||
bus.RunCommand(commands.Assign{
|
||||
Match: formattools.CompactMatch(matched),
|
||||
Effect: line.Assign.Effect.Effect,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if line.Set != nil {
|
||||
switch line.Set.Scope {
|
||||
case "devices":
|
||||
bus.RunCommand(SetVariable{
|
||||
Devices: gentools.Map(devices, func(d device.Pointer) string {
|
||||
return d.ID
|
||||
}),
|
||||
Key: line.Set.Key,
|
||||
Value: line.Set.Value,
|
||||
})
|
||||
case "match":
|
||||
bus.RunCommand(SetVariable{
|
||||
Match: gentools.Ptr(match),
|
||||
Key: line.Set.Key,
|
||||
Value: line.Set.Value,
|
||||
})
|
||||
case "global":
|
||||
bus.RunCommand(SetVariable{
|
||||
Key: line.Set.Key,
|
||||
Value: line.Set.Value,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package script
|
||||
|
||||
import (
|
||||
lucifer3 "git.aiterp.net/lucifer3/server"
|
||||
"git.aiterp.net/lucifer3/server/internal/gentools"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type VariableSet map[string]map[string]string
|
||||
|
||||
func (v VariableSet) Global(key string) string {
|
||||
return v["global"][key]
|
||||
}
|
||||
|
||||
func (v VariableSet) Match(tag string, key string) string {
|
||||
return v["match:"+tag][key]
|
||||
}
|
||||
|
||||
func (v VariableSet) Devices(list []string, key string) string {
|
||||
return v["devices:"+strings.Join(list, ";")][key]
|
||||
}
|
||||
|
||||
type Variables struct {
|
||||
mu sync.Mutex
|
||||
vars VariableSet
|
||||
}
|
||||
|
||||
func NewVariables() *Variables {
|
||||
return &Variables{
|
||||
vars: make(VariableSet, 8),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Variables) Get() VariableSet {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
return s.vars
|
||||
}
|
||||
|
||||
func (s *Variables) Active() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Variables) HandleEvent(_ *lucifer3.EventBus, _ lucifer3.Event) {}
|
||||
|
||||
func (s *Variables) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command) {
|
||||
if sv, ok := command.(SetVariable); ok {
|
||||
s.mu.Lock()
|
||||
curr := gentools.CopyMap(s.vars)
|
||||
s.mu.Unlock()
|
||||
|
||||
key := "global"
|
||||
if sv.Match != nil {
|
||||
key = "match:" + *sv.Match
|
||||
} else if sv.Devices != nil {
|
||||
key = "devices:" + strings.Join(sv.Devices, ";")
|
||||
}
|
||||
|
||||
curr[key] = gentools.CopyMap(curr["global"])
|
||||
|
||||
if sv.Value == "" {
|
||||
delete(curr[key], sv.Key)
|
||||
} else {
|
||||
curr[key][sv.Key] = sv.Value
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.vars = curr
|
||||
s.mu.Unlock()
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"git.aiterp.net/lucifer3/server/internal/color"
|
||||
"git.aiterp.net/lucifer3/server/internal/gentools"
|
||||
"github.com/eriklupander/tradfri-go/tradfri"
|
||||
"github.com/pkg/errors"
|
||||
"math"
|
||||
"math/rand"
|
||||
"strings"
|
||||
@@ -77,7 +78,7 @@ func (b *Bridge) listen(bus *lucifer3.EventBus) {
|
||||
if err != nil {
|
||||
bus.RunEvent(events.DeviceFailed{
|
||||
ID: b.id,
|
||||
Error: "Unable to fetch IKEA devices",
|
||||
Error: "Unable to fetch IKEA devices: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -243,7 +244,7 @@ const defaultCF = device.CFlagXY
|
||||
|
||||
func catchPanic(mutexes ...sync.Locker) (retErr error) {
|
||||
if err, ok := recover().(error); ok {
|
||||
retErr = err
|
||||
retErr = errors.Wrap(err, "panik:")
|
||||
}
|
||||
|
||||
for _, mx := range mutexes {
|
||||
|
||||
@@ -6,12 +6,14 @@ import (
|
||||
"git.aiterp.net/lucifer3/server/events"
|
||||
"git.aiterp.net/lucifer3/server/internal/color"
|
||||
"git.aiterp.net/lucifer3/server/internal/gentools"
|
||||
"git.aiterp.net/lucifer3/server/services/script"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Data struct {
|
||||
Devices map[string]Device `json:"devices"`
|
||||
Assignments map[uuid.UUID]Assignment `json:"assignments"`
|
||||
Scripts map[string][]script.Line `json:"scripts"`
|
||||
}
|
||||
|
||||
func (d *Data) WithPatch(patches ...Patch) Data {
|
||||
@@ -89,6 +91,10 @@ func (d *Data) WithPatch(patches ...Patch) Data {
|
||||
newData.Assignments[pa.ID] = pa
|
||||
}
|
||||
}
|
||||
|
||||
if patch.Script != nil {
|
||||
newData.Scripts[patch.Script.Name] = patch.Script.Lines
|
||||
}
|
||||
}
|
||||
|
||||
return newData
|
||||
@@ -98,6 +104,7 @@ func (d *Data) Copy() Data {
|
||||
return Data{
|
||||
Devices: gentools.CopyMap(d.Devices),
|
||||
Assignments: gentools.CopyMap(d.Assignments),
|
||||
Scripts: gentools.CopyMap(d.Scripts),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,14 @@ import (
|
||||
"git.aiterp.net/lucifer3/server/effects"
|
||||
"git.aiterp.net/lucifer3/server/events"
|
||||
"git.aiterp.net/lucifer3/server/internal/color"
|
||||
"git.aiterp.net/lucifer3/server/services/script"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Patch struct {
|
||||
Assignment *AssignmentPatch `json:"assignment,omitempty"`
|
||||
Device *DevicePatch `json:"device,omitempty"`
|
||||
Script *ScriptPatch `json:"script,omitempty"`
|
||||
}
|
||||
|
||||
func (e Patch) VerboseKey() string {
|
||||
@@ -41,6 +43,8 @@ func (e Patch) EventDescription() string {
|
||||
}
|
||||
} else if e.Assignment != nil {
|
||||
return fmt.Sprintf("uistate.Patch(assignment=%s)", e.Assignment.ID)
|
||||
} else if e.Script != nil {
|
||||
return fmt.Sprintf("uistate.Patch(script=%s)", e.Script.Name)
|
||||
} else {
|
||||
return "uistate.Patch"
|
||||
}
|
||||
@@ -71,3 +75,8 @@ type AssignmentPatch struct {
|
||||
Variables map[string]float64 `json:"variables,omitempty"`
|
||||
Delete bool `json:"delete,omitempty"`
|
||||
}
|
||||
|
||||
type ScriptPatch struct {
|
||||
Name string
|
||||
Lines []script.Line
|
||||
}
|
||||
|
||||
@@ -6,11 +6,19 @@ import (
|
||||
"git.aiterp.net/lucifer3/server/effects"
|
||||
"git.aiterp.net/lucifer3/server/events"
|
||||
"git.aiterp.net/lucifer3/server/internal/gentools"
|
||||
"git.aiterp.net/lucifer3/server/services/script"
|
||||
"github.com/google/uuid"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func NewService() lucifer3.ActiveService {
|
||||
return &service{}
|
||||
return &service{
|
||||
data: Data{
|
||||
Devices: map[string]Device{},
|
||||
Assignments: map[uuid.UUID]Assignment{},
|
||||
Scripts: map[string][]script.Line{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type service struct {
|
||||
@@ -35,6 +43,10 @@ func (s *service) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command
|
||||
Device: &DevicePatch{ID: id, DesiredState: gentools.ShallowCopy(&state)},
|
||||
})
|
||||
}
|
||||
case script.UpdateScript:
|
||||
patches = append(patches, Patch{
|
||||
Script: &ScriptPatch{Name: command.Name, Lines: command.Lines},
|
||||
})
|
||||
}
|
||||
|
||||
if len(patches) > 0 {
|
||||
|
||||
Reference in New Issue
Block a user