From e8b77ae69c719786568e9ddb6924131bf39ed917 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Tue, 29 Aug 2023 21:00:29 +0200 Subject: [PATCH] add script triggers. --- cmd/lucifer4-server/main.go | 1 + events/log.go | 2 +- events/time.go | 16 ++++ services/httpapiv1/service.go | 27 ++++++- .../20230829192814_script_trigger.sql | 18 +++++ services/mysqldb/mysqlgen/db.go | 30 ++++++++ services/mysqldb/mysqlgen/models.go | 11 +++ services/mysqldb/mysqlgen/script.sql.go | 77 +++++++++++++++++++ services/mysqldb/queries/script.sql | 12 ++- services/mysqldb/service.go | 72 ++++++++++++++++- services/resolver.go | 4 + services/script/commands.go | 35 ++++++--- services/script/service.go | 40 +++++++--- services/script/trigger.go | 70 +++++++++++++++++ services/ticker.go | 44 +++++++++++ services/uistate/service.go | 2 +- 16 files changed, 434 insertions(+), 27 deletions(-) create mode 100644 events/time.go create mode 100644 services/mysqldb/migrations/20230829192814_script_trigger.sql create mode 100644 services/script/trigger.go create mode 100644 services/ticker.go diff --git a/cmd/lucifer4-server/main.go b/cmd/lucifer4-server/main.go index f9bcdd8..63a8a9b 100644 --- a/cmd/lucifer4-server/main.go +++ b/cmd/lucifer4-server/main.go @@ -42,6 +42,7 @@ func main() { } bus.JoinPrivileged(resolver) + bus.Join(services.NewTicker()) bus.Join(effectenforcer.NewService(resolver)) bus.Join(nanoleaf.NewService()) bus.Join(hue.NewService()) diff --git a/events/log.go b/events/log.go index 0c247cf..7559c2c 100644 --- a/events/log.go +++ b/events/log.go @@ -10,5 +10,5 @@ type Log struct { } func (l Log) EventDescription() string { - return fmt.Sprintf("LOG(id:%s level:%s code:%s :: %s)", l.ID, l.Level, l.Code, l.Message) + return fmt.Sprintf("Log(id:%s level:%s code:%s :: %s)", l.ID, l.Level, l.Code, l.Message) } diff --git a/events/time.go b/events/time.go new file mode 100644 index 0000000..fee2771 --- /dev/null +++ b/events/time.go @@ -0,0 +1,16 @@ +package events + +import "fmt" + +type Time struct { + Hour int + Minute int +} + +func (t Time) EventDescription() string { + return fmt.Sprintf("Time(%02d:%02d)", t.Hour, t.Minute) +} + +func (t Time) VerboseKey() string { + return "time" +} diff --git a/services/httpapiv1/service.go b/services/httpapiv1/service.go index 5d5d6ae..1b1bdb6 100644 --- a/services/httpapiv1/service.go +++ b/services/httpapiv1/service.go @@ -7,6 +7,7 @@ import ( "git.aiterp.net/lucifer3/server/events" "git.aiterp.net/lucifer3/server/services/script" "git.aiterp.net/lucifer3/server/services/uistate" + "github.com/google/uuid" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "log" @@ -14,6 +15,13 @@ import ( "sync" ) +var zeroUUID = uuid.UUID{ + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, +} + func New(addr string) (lucifer3.Service, error) { svc := &service{} @@ -67,6 +75,19 @@ func New(addr string) (lucifer3.Service, error) { bus.RunCommand(*input.UpdateScript) case input.ExecuteScript != nil: bus.RunCommand(*input.ExecuteScript) + case input.UpdateTrigger != nil: + if input.UpdateTrigger.ID == zeroUUID { + input.UpdateTrigger.ID = uuid.New() + } + if input.UpdateTrigger.ScriptPre == nil { + input.UpdateTrigger.ScriptPre = make([]script.Line, 0) + } + if input.UpdateTrigger.ScriptPost == nil { + input.UpdateTrigger.ScriptPost = make([]script.Line, 0) + } + bus.RunCommand(*input.UpdateTrigger) + case input.DeleteTrigger != nil: + bus.RunCommand(*input.DeleteTrigger) default: return c.String(400, "No supported command found in input") } @@ -123,8 +144,10 @@ type commandInput struct { 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"` + UpdateScript *script.Update `json:"updateScript,omitempty"` + ExecuteScript *script.Execute `json:"executeScript,omitempty"` + UpdateTrigger *script.UpdateTrigger `json:"updateTrigger,omitempty"` + DeleteTrigger *script.DeleteTrigger `json:"deleteTrigger,omitempty"` } type assignInput struct { diff --git a/services/mysqldb/migrations/20230829192814_script_trigger.sql b/services/mysqldb/migrations/20230829192814_script_trigger.sql new file mode 100644 index 0000000..a6a2ddf --- /dev/null +++ b/services/mysqldb/migrations/20230829192814_script_trigger.sql @@ -0,0 +1,18 @@ +-- +goose Up +-- +goose StatementBegin +CREATE TABLE script_trigger ( + id CHAR(36) NOT NULL PRIMARY KEY, + event VARCHAR(255) NOT NULL, + device_match TEXT NOT NULL, + parameter TEXT NOT NULL, + script_target TEXT NOT NULL, + script_name VARCHAR(255) NOT NULL, + script_pre JSON NOT NULL, + script_post JSON NOT NULL +) +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS script_trigger; +-- +goose StatementEnd diff --git a/services/mysqldb/mysqlgen/db.go b/services/mysqldb/mysqlgen/db.go index a7338dd..9b340e1 100644 --- a/services/mysqldb/mysqlgen/db.go +++ b/services/mysqldb/mysqlgen/db.go @@ -48,6 +48,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.deleteDeviceInfoLikeStmt, err = db.PrepareContext(ctx, deleteDeviceInfoLike); err != nil { return nil, fmt.Errorf("error preparing query DeleteDeviceInfoLike: %w", err) } + if q.deleteScriptTriggerStmt, err = db.PrepareContext(ctx, deleteScriptTrigger); err != nil { + return nil, fmt.Errorf("error preparing query DeleteScriptTrigger: %w", err) + } if q.insertDeviceAliasStmt, err = db.PrepareContext(ctx, insertDeviceAlias); err != nil { return nil, fmt.Errorf("error preparing query InsertDeviceAlias: %w", err) } @@ -63,6 +66,9 @@ 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.listScriptTriggersStmt, err = db.PrepareContext(ctx, listScriptTriggers); err != nil { + return nil, fmt.Errorf("error preparing query ListScriptTriggers: %w", err) + } if q.listScriptVariablesStmt, err = db.PrepareContext(ctx, listScriptVariables); err != nil { return nil, fmt.Errorf("error preparing query ListScriptVariables: %w", err) } @@ -78,6 +84,9 @@ 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.replaceScriptTriggerStmt, err = db.PrepareContext(ctx, replaceScriptTrigger); err != nil { + return nil, fmt.Errorf("error preparing query ReplaceScriptTrigger: %w", err) + } if q.saveScriptStmt, err = db.PrepareContext(ctx, saveScript); err != nil { return nil, fmt.Errorf("error preparing query SaveScript: %w", err) } @@ -129,6 +138,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing deleteDeviceInfoLikeStmt: %w", cerr) } } + if q.deleteScriptTriggerStmt != nil { + if cerr := q.deleteScriptTriggerStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteScriptTriggerStmt: %w", cerr) + } + } if q.insertDeviceAliasStmt != nil { if cerr := q.insertDeviceAliasStmt.Close(); cerr != nil { err = fmt.Errorf("error closing insertDeviceAliasStmt: %w", cerr) @@ -154,6 +168,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing listDeviceInfosStmt: %w", cerr) } } + if q.listScriptTriggersStmt != nil { + if cerr := q.listScriptTriggersStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listScriptTriggersStmt: %w", cerr) + } + } if q.listScriptVariablesStmt != nil { if cerr := q.listScriptVariablesStmt.Close(); cerr != nil { err = fmt.Errorf("error closing listScriptVariablesStmt: %w", cerr) @@ -179,6 +198,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing replaceDeviceInfoStmt: %w", cerr) } } + if q.replaceScriptTriggerStmt != nil { + if cerr := q.replaceScriptTriggerStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing replaceScriptTriggerStmt: %w", cerr) + } + } if q.saveScriptStmt != nil { if cerr := q.saveScriptStmt.Close(); cerr != nil { err = fmt.Errorf("error closing saveScriptStmt: %w", cerr) @@ -236,16 +260,19 @@ type Queries struct { deleteDeviceInfoStmt *sql.Stmt deleteDeviceInfoByIDStmt *sql.Stmt deleteDeviceInfoLikeStmt *sql.Stmt + deleteScriptTriggerStmt *sql.Stmt insertDeviceAliasStmt *sql.Stmt listDeviceAliasesStmt *sql.Stmt listDeviceAssignmentsStmt *sql.Stmt listDeviceAuthStmt *sql.Stmt listDeviceInfosStmt *sql.Stmt + listScriptTriggersStmt *sql.Stmt listScriptVariablesStmt *sql.Stmt listScriptsStmt *sql.Stmt replaceDeviceAssignmentStmt *sql.Stmt replaceDeviceAuthStmt *sql.Stmt replaceDeviceInfoStmt *sql.Stmt + replaceScriptTriggerStmt *sql.Stmt saveScriptStmt *sql.Stmt updateScriptVariablesStmt *sql.Stmt } @@ -262,16 +289,19 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { deleteDeviceInfoStmt: q.deleteDeviceInfoStmt, deleteDeviceInfoByIDStmt: q.deleteDeviceInfoByIDStmt, deleteDeviceInfoLikeStmt: q.deleteDeviceInfoLikeStmt, + deleteScriptTriggerStmt: q.deleteScriptTriggerStmt, insertDeviceAliasStmt: q.insertDeviceAliasStmt, listDeviceAliasesStmt: q.listDeviceAliasesStmt, listDeviceAssignmentsStmt: q.listDeviceAssignmentsStmt, listDeviceAuthStmt: q.listDeviceAuthStmt, listDeviceInfosStmt: q.listDeviceInfosStmt, + listScriptTriggersStmt: q.listScriptTriggersStmt, listScriptVariablesStmt: q.listScriptVariablesStmt, listScriptsStmt: q.listScriptsStmt, replaceDeviceAssignmentStmt: q.replaceDeviceAssignmentStmt, replaceDeviceAuthStmt: q.replaceDeviceAuthStmt, replaceDeviceInfoStmt: q.replaceDeviceInfoStmt, + replaceScriptTriggerStmt: q.replaceScriptTriggerStmt, saveScriptStmt: q.saveScriptStmt, updateScriptVariablesStmt: q.updateScriptVariablesStmt, } diff --git a/services/mysqldb/mysqlgen/models.go b/services/mysqldb/mysqlgen/models.go index fe5fa71..87b85c0 100644 --- a/services/mysqldb/mysqlgen/models.go +++ b/services/mysqldb/mysqlgen/models.go @@ -40,6 +40,17 @@ type Script struct { Data json.RawMessage } +type ScriptTrigger struct { + ID uuid.UUID + Event string + DeviceMatch string + Parameter string + ScriptTarget string + ScriptName string + ScriptPre json.RawMessage + ScriptPost json.RawMessage +} + type ScriptVariable struct { Scope string Name string diff --git a/services/mysqldb/mysqlgen/script.sql.go b/services/mysqldb/mysqlgen/script.sql.go index 6e4c110..640a895 100644 --- a/services/mysqldb/mysqlgen/script.sql.go +++ b/services/mysqldb/mysqlgen/script.sql.go @@ -8,8 +8,55 @@ package mysqlgen import ( "context" "encoding/json" + + "github.com/google/uuid" ) +const deleteScriptTrigger = `-- name: DeleteScriptTrigger :exec +DELETE FROM script_trigger WHERE id = ? +` + +func (q *Queries) DeleteScriptTrigger(ctx context.Context, id uuid.UUID) error { + _, err := q.exec(ctx, q.deleteScriptTriggerStmt, deleteScriptTrigger, id) + return err +} + +const listScriptTriggers = `-- name: ListScriptTriggers :many +SELECT id, event, device_match, parameter, script_target, script_name, script_pre, script_post FROM script_trigger +` + +func (q *Queries) ListScriptTriggers(ctx context.Context) ([]ScriptTrigger, error) { + rows, err := q.query(ctx, q.listScriptTriggersStmt, listScriptTriggers) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ScriptTrigger{} + for rows.Next() { + var i ScriptTrigger + if err := rows.Scan( + &i.ID, + &i.Event, + &i.DeviceMatch, + &i.Parameter, + &i.ScriptTarget, + &i.ScriptName, + &i.ScriptPre, + &i.ScriptPost, + ); 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 listScriptVariables = `-- name: ListScriptVariables :many SELECT scope, name, value FROM script_variable ` @@ -64,6 +111,36 @@ func (q *Queries) ListScripts(ctx context.Context) ([]Script, error) { return items, nil } +const replaceScriptTrigger = `-- name: ReplaceScriptTrigger :exec +REPLACE INTO script_trigger (id, event, device_match, parameter, script_target, script_name, script_pre, script_post) +VALUES (?, ?, ?, ?, ?, ?, ?, ?) +` + +type ReplaceScriptTriggerParams struct { + ID uuid.UUID + Event string + DeviceMatch string + Parameter string + ScriptTarget string + ScriptName string + ScriptPre json.RawMessage + ScriptPost json.RawMessage +} + +func (q *Queries) ReplaceScriptTrigger(ctx context.Context, arg ReplaceScriptTriggerParams) error { + _, err := q.exec(ctx, q.replaceScriptTriggerStmt, replaceScriptTrigger, + arg.ID, + arg.Event, + arg.DeviceMatch, + arg.Parameter, + arg.ScriptTarget, + arg.ScriptName, + arg.ScriptPre, + arg.ScriptPost, + ) + return err +} + const saveScript = `-- name: SaveScript :exec REPLACE INTO script (name, data) VALUES (?, ?) ` diff --git a/services/mysqldb/queries/script.sql b/services/mysqldb/queries/script.sql index 67d0db6..a4b8dec 100644 --- a/services/mysqldb/queries/script.sql +++ b/services/mysqldb/queries/script.sql @@ -8,4 +8,14 @@ REPLACE INTO script (name, data) VALUES (?, ?); SELECT * FROM script_variable; -- name: UpdateScriptVariables :exec -REPLACE INTO script_variable (scope, name, value) VALUES (?, ?, ?); \ No newline at end of file +REPLACE INTO script_variable (scope, name, value) VALUES (?, ?, ?); + +-- name: ListScriptTriggers :many +SELECT * FROM script_trigger; + +-- name: ReplaceScriptTrigger :exec +REPLACE INTO script_trigger (id, event, device_match, parameter, script_target, script_name, script_pre, script_post) +VALUES (?, ?, ?, ?, ?, ?, ?, ?); + +-- name: DeleteScriptTrigger :exec +DELETE FROM script_trigger WHERE id = ?; \ No newline at end of file diff --git a/services/mysqldb/service.go b/services/mysqldb/service.go index 3a2debf..83a8c3c 100644 --- a/services/mysqldb/service.go +++ b/services/mysqldb/service.go @@ -141,7 +141,7 @@ func (d *database) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Event) { }) } - bus.RunCommand(script.UpdateScript{ + bus.RunCommand(script.Update{ Name: scr.Name, Lines: lines, }) @@ -174,6 +174,26 @@ func (d *database) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Event) { bus.RunCommand(cmd) } + scriptTriggers, err := q.ListScriptTriggers(timeout) + if err != nil { + bus.RunEvent(events.Log{ + Level: "error", + Code: "database_could_not_list_script_triggers", + Message: "Database could not list script triggers: " + err.Error(), + }) + } + for _, trig := range scriptTriggers { + bus.RunCommand(script.UpdateTrigger{ + ID: trig.ID, + Event: script.TriggerEvent(trig.Event), + DeviceMatch: trig.DeviceMatch, + Parameter: trig.Parameter, + ScriptTarget: trig.ScriptTarget, + ScriptName: trig.ScriptName, + ScriptPre: fromJSON[[]script.Line](trig.ScriptPre), + ScriptPost: fromJSON[[]script.Line](trig.ScriptPost), + }) + } case events.DeviceAccepted: if event.Extras == nil { event.Extras = map[string]string{} @@ -359,6 +379,33 @@ func (d *database) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Comman defer cancel() switch command := command.(type) { + case script.UpdateTrigger: + err := q.ReplaceScriptTrigger(timeout, mysqlgen.ReplaceScriptTriggerParams{ + ID: command.ID, + Event: string(command.Event), + DeviceMatch: command.DeviceMatch, + Parameter: command.Parameter, + ScriptTarget: command.ScriptTarget, + ScriptName: command.ScriptName, + ScriptPre: toJSON(command.ScriptPre), + ScriptPost: toJSON(command.ScriptPost), + }) + if err != nil { + bus.RunEvent(events.Log{ + Level: "error", + Code: "database_could_not_save_trigger", + Message: "Failed to save trigger: " + err.Error(), + }) + } + case script.DeleteTrigger: + err := q.DeleteScriptTrigger(timeout, command.ID) + if err != nil { + bus.RunEvent(events.Log{ + Level: "error", + Code: "database_could_not_delete_trigger", + Message: "Failed to delete trigger: " + err.Error(), + }) + } case script.SetVariable: scopeName := "global" if command.Match != nil { @@ -368,13 +415,21 @@ func (d *database) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Comman } if d.scriptVariables[scopeName+"::"+command.Key] != command.Value { - _ = q.UpdateScriptVariables(timeout, mysqlgen.UpdateScriptVariablesParams{ + d.scriptVariables[scopeName+"::"+command.Key] = command.Value + err := q.UpdateScriptVariables(timeout, mysqlgen.UpdateScriptVariablesParams{ Scope: scopeName, Name: command.Key, Value: command.Value, }) + if err != nil { + bus.RunEvent(events.Log{ + Level: "error", + Code: "database_could_not_save_variables", + Message: "Failed to save variable: " + err.Error(), + }) + } } - case script.UpdateScript: + case script.Update: j, _ := json.Marshal(command.Lines) err := q.SaveScript(timeout, mysqlgen.SaveScriptParams{ @@ -391,6 +446,17 @@ func (d *database) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Comman } } +func fromJSON[T any](j []byte) T { + var t T + _ = json.Unmarshal(j, &t) + return t +} + +func toJSON[T any](v T) json.RawMessage { + j, _ := json.Marshal(v) + return j +} + func Connect(host string, port int, username, password, dbname string) (lucifer3.ActiveService, error) { db, err := sql.Open("mysql", fmt.Sprintf( "%s:%s@(%s:%d)/%s?parseTime=true", username, password, host, port, dbname, diff --git a/services/resolver.go b/services/resolver.go index 7bf0c69..e691006 100644 --- a/services/resolver.go +++ b/services/resolver.go @@ -56,6 +56,10 @@ func (r *Resolver) GetByID(id string) *device.Pointer { } func (r *Resolver) Resolve(pattern string) []device.Pointer { + if pattern == "" { + return []device.Pointer{} + } + r.mu.Lock() defer r.mu.Unlock() diff --git a/services/script/commands.go b/services/script/commands.go index 7d1c67e..c590221 100644 --- a/services/script/commands.go +++ b/services/script/commands.go @@ -2,9 +2,26 @@ package script import ( "fmt" + "github.com/google/uuid" "strings" ) +type UpdateTrigger Trigger + +func (c UpdateTrigger) CommandDescription() string { + return fmt.Sprintf("script.UpdateTrigger(%s, %s(%#+v, %#+v))", + c.ID, c.Event, c.DeviceMatch, c.Parameter, + ) +} + +type DeleteTrigger struct { + ID uuid.UUID `json:"id"` +} + +func (c DeleteTrigger) CommandDescription() string { + return fmt.Sprintf("script.DeleteTrigger(%s)", c.ID) +} + type SetVariable struct { Match *string Devices []string @@ -15,28 +32,28 @@ type SetVariable struct { func (c SetVariable) CommandDescription() string { switch { case c.Match != nil: - return fmt.Sprintf("SetVariable(tag(%s), %s, %s)", *c.Match, c.Key, c.Value) + return fmt.Sprintf("script.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) + return fmt.Sprintf("script.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) + return fmt.Sprintf("script.SetVariable(global, %s, %s)", c.Key, c.Value) } } -type UpdateScript struct { +type Update 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)) +func (c Update) CommandDescription() string { + return fmt.Sprintf("script.Update(%s, [%d lines...])", c.Name, len(c.Lines)) } -type ExecuteScript struct { +type Execute struct { Name string `json:"name"` Match string `json:"match"` } -func (c ExecuteScript) CommandDescription() string { - return fmt.Sprintf("ExecuteScript(%s, %s)", c.Name, c.Match) +func (c Execute) CommandDescription() string { + return fmt.Sprintf("script.Execute(%s, %s)", c.Name, c.Match) } diff --git a/services/script/service.go b/services/script/service.go index 910698a..7453faa 100644 --- a/services/script/service.go +++ b/services/script/service.go @@ -6,36 +6,51 @@ import ( "git.aiterp.net/lucifer3/server/device" "git.aiterp.net/lucifer3/server/internal/formattools" "git.aiterp.net/lucifer3/server/internal/gentools" + "github.com/google/uuid" "sort" - "sync" ) type service struct { - mu sync.Mutex resolver device.Resolver variables *Variables scripts map[string]*Script + triggers map[uuid.UUID]*Trigger } func (s *service) Active() bool { return true } -func (s *service) HandleEvent(_ *lucifer3.EventBus, _ lucifer3.Event) {} +func (s *service) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Event) { + for _, trig := range s.triggers { + if trig.Check(event, s.resolver) { + lines := append(make([]Line, 0, 64), trig.ScriptPre...) + script := s.scripts[trig.ScriptName] + if script != nil { + lines = append(lines, script.Lines...) + } + lines = append(lines, trig.ScriptPost...) + + variables := s.variables.Get() + devices := s.resolver.Resolve(trig.ScriptTarget) + sort.Slice(devices, func(i, j int) bool { + return devices[i].ID < devices[j].ID + }) + + s.runScript(bus, script.Lines, trig.ScriptTarget, map[string]bool{}, devices, variables) + } + } +} func (s *service) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command) { switch command := command.(type) { - case UpdateScript: - s.mu.Lock() + case Update: s.scripts[command.Name] = &Script{ Name: command.Name, Lines: command.Lines, } - s.mu.Unlock() - case ExecuteScript: - s.mu.Lock() + case Execute: script := s.scripts[command.Name] - s.mu.Unlock() if script == nil { break } @@ -48,6 +63,10 @@ func (s *service) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command }) s.runScript(bus, script.Lines, command.Match, map[string]bool{}, devices, variables) + case UpdateTrigger: + s.triggers[command.ID] = gentools.Ptr(Trigger(command)) + case DeleteTrigger: + delete(s.triggers, command.ID) } } @@ -55,7 +74,8 @@ func NewService(resolver device.Resolver, variables *Variables) lucifer3.Service return &service{ resolver: resolver, variables: variables, - scripts: map[string]*Script{}, + triggers: make(map[uuid.UUID]*Trigger, 8), + scripts: make(map[string]*Script, 8), } } diff --git a/services/script/trigger.go b/services/script/trigger.go new file mode 100644 index 0000000..6701270 --- /dev/null +++ b/services/script/trigger.go @@ -0,0 +1,70 @@ +package script + +import ( + "fmt" + lucifer3 "git.aiterp.net/lucifer3/server" + "git.aiterp.net/lucifer3/server/device" + "git.aiterp.net/lucifer3/server/events" + "github.com/google/uuid" + "strconv" +) + +type TriggerEvent string + +const ( + TEButton TriggerEvent = "Button" + TETime TriggerEvent = "Time" + TETemperatureAbove TriggerEvent = "TemperatureAbove" + TETemperatureBelow TriggerEvent = "TemperatureBelow" +) + +type Trigger struct { + ID uuid.UUID `json:"id"` + Event TriggerEvent `json:"event"` + DeviceMatch string `json:"deviceMatch"` + Parameter string `json:"parameter"` + ScriptTarget string `json:"scriptTarget"` + ScriptName string `json:"scriptName"` + ScriptPre []Line `json:"scriptPre"` + ScriptPost []Line `json:"scriptPost"` +} + +func (t *Trigger) Check(event lucifer3.Event, res device.Resolver) bool { + devices := res.Resolve(t.DeviceMatch) + + switch event := event.(type) { + case events.ButtonPressed: + if t.Event != TEButton { + return false + } + + for _, dev := range devices { + if dev.ID == event.ID { + return t.Parameter == event.Name + } + } + case events.Time: + if t.Event != TETime { + return false + } + + return fmt.Sprintf("%02d:%02d", event.Hour, event.Minute) == t.Parameter + case events.TemperatureChanged: + if t.Event != TETemperatureAbove && t.Event != TETemperatureBelow { + return false + } + + num, err := strconv.ParseFloat(t.Parameter, 64) + if err != nil { + return false + } + + for _, dev := range devices { + if dev.ID == event.ID { + return (event.Temperature >= num) == (t.Event == TETemperatureAbove) + } + } + } + + return false +} diff --git a/services/ticker.go b/services/ticker.go new file mode 100644 index 0000000..80d0708 --- /dev/null +++ b/services/ticker.go @@ -0,0 +1,44 @@ +package services + +import ( + lucifer3 "git.aiterp.net/lucifer3/server" + "git.aiterp.net/lucifer3/server/events" + "time" +) + +func NewTicker() lucifer3.Service { + return &tickerService{} +} + +type tickerService struct{} + +func (s *tickerService) Active() bool { + return true +} + +func (s *tickerService) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Event) { + if _, ok := event.(events.Started); ok { + go func() { + now := time.Now() + bus.RunEvent(events.Time{ + Hour: now.Hour(), + Minute: now.Minute(), + }) + + time.Sleep(time.Now().Truncate(time.Minute).Add(time.Minute).Sub(time.Now())) + + now = time.Now() + bus.RunEvent(events.Time{ + Hour: now.Hour(), + Minute: now.Minute(), + }) + + for t := range time.NewTicker(time.Minute).C { + bus.RunEvent(events.Time{ + Hour: t.Hour(), + Minute: t.Minute(), + }) + } + }() + } +} diff --git a/services/uistate/service.go b/services/uistate/service.go index 2336430..2371b38 100644 --- a/services/uistate/service.go +++ b/services/uistate/service.go @@ -43,7 +43,7 @@ func (s *service) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command Device: &DevicePatch{ID: id, DesiredState: gentools.ShallowCopy(&state)}, }) } - case script.UpdateScript: + case script.Update: patches = append(patches, Patch{ Script: &ScriptPatch{Name: command.Name, Lines: command.Lines}, })