Browse Source

add script triggers.

beelzebub
Gisle Aune 1 year ago
parent
commit
e8b77ae69c
  1. 1
      cmd/lucifer4-server/main.go
  2. 2
      events/log.go
  3. 16
      events/time.go
  4. 27
      services/httpapiv1/service.go
  5. 18
      services/mysqldb/migrations/20230829192814_script_trigger.sql
  6. 30
      services/mysqldb/mysqlgen/db.go
  7. 11
      services/mysqldb/mysqlgen/models.go
  8. 77
      services/mysqldb/mysqlgen/script.sql.go
  9. 10
      services/mysqldb/queries/script.sql
  10. 72
      services/mysqldb/service.go
  11. 4
      services/resolver.go
  12. 35
      services/script/commands.go
  13. 40
      services/script/service.go
  14. 70
      services/script/trigger.go
  15. 44
      services/ticker.go
  16. 2
      services/uistate/service.go

1
cmd/lucifer4-server/main.go

@ -42,6 +42,7 @@ func main() {
} }
bus.JoinPrivileged(resolver) bus.JoinPrivileged(resolver)
bus.Join(services.NewTicker())
bus.Join(effectenforcer.NewService(resolver)) bus.Join(effectenforcer.NewService(resolver))
bus.Join(nanoleaf.NewService()) bus.Join(nanoleaf.NewService())
bus.Join(hue.NewService()) bus.Join(hue.NewService())

2
events/log.go

@ -10,5 +10,5 @@ type Log struct {
} }
func (l Log) EventDescription() string { 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)
} }

16
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"
}

27
services/httpapiv1/service.go

@ -7,6 +7,7 @@ import (
"git.aiterp.net/lucifer3/server/events" "git.aiterp.net/lucifer3/server/events"
"git.aiterp.net/lucifer3/server/services/script" "git.aiterp.net/lucifer3/server/services/script"
"git.aiterp.net/lucifer3/server/services/uistate" "git.aiterp.net/lucifer3/server/services/uistate"
"github.com/google/uuid"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v4/middleware"
"log" "log"
@ -14,6 +15,13 @@ import (
"sync" "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) { func New(addr string) (lucifer3.Service, error) {
svc := &service{} svc := &service{}
@ -67,6 +75,19 @@ func New(addr string) (lucifer3.Service, error) {
bus.RunCommand(*input.UpdateScript) bus.RunCommand(*input.UpdateScript)
case input.ExecuteScript != nil: case input.ExecuteScript != nil:
bus.RunCommand(*input.ExecuteScript) 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: default:
return c.String(400, "No supported command found in input") return c.String(400, "No supported command found in input")
} }
@ -123,8 +144,10 @@ type commandInput struct {
ConnectDevice *commands.ConnectDevice `json:"connectDevice,omitempty"` ConnectDevice *commands.ConnectDevice `json:"connectDevice,omitempty"`
SearchDevices *commands.SearchDevices `json:"searchDevices,omitempty"` SearchDevices *commands.SearchDevices `json:"searchDevices,omitempty"`
ForgetDevice *commands.ForgetDevice `json:"forgetDevice,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 { type assignInput struct {

18
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

30
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 { if q.deleteDeviceInfoLikeStmt, err = db.PrepareContext(ctx, deleteDeviceInfoLike); err != nil {
return nil, fmt.Errorf("error preparing query DeleteDeviceInfoLike: %w", err) 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 { if q.insertDeviceAliasStmt, err = db.PrepareContext(ctx, insertDeviceAlias); err != nil {
return nil, fmt.Errorf("error preparing query InsertDeviceAlias: %w", err) 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 { if q.listDeviceInfosStmt, err = db.PrepareContext(ctx, listDeviceInfos); err != nil {
return nil, fmt.Errorf("error preparing query ListDeviceInfos: %w", err) 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 { if q.listScriptVariablesStmt, err = db.PrepareContext(ctx, listScriptVariables); err != nil {
return nil, fmt.Errorf("error preparing query ListScriptVariables: %w", err) 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 { if q.replaceDeviceInfoStmt, err = db.PrepareContext(ctx, replaceDeviceInfo); err != nil {
return nil, fmt.Errorf("error preparing query ReplaceDeviceInfo: %w", err) 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 { if q.saveScriptStmt, err = db.PrepareContext(ctx, saveScript); err != nil {
return nil, fmt.Errorf("error preparing query SaveScript: %w", err) 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) 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 q.insertDeviceAliasStmt != nil {
if cerr := q.insertDeviceAliasStmt.Close(); cerr != nil { if cerr := q.insertDeviceAliasStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing insertDeviceAliasStmt: %w", cerr) 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) 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 q.listScriptVariablesStmt != nil {
if cerr := q.listScriptVariablesStmt.Close(); cerr != nil { if cerr := q.listScriptVariablesStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing listScriptVariablesStmt: %w", cerr) 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) 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 q.saveScriptStmt != nil {
if cerr := q.saveScriptStmt.Close(); cerr != nil { if cerr := q.saveScriptStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing saveScriptStmt: %w", cerr) err = fmt.Errorf("error closing saveScriptStmt: %w", cerr)
@ -236,16 +260,19 @@ type Queries struct {
deleteDeviceInfoStmt *sql.Stmt deleteDeviceInfoStmt *sql.Stmt
deleteDeviceInfoByIDStmt *sql.Stmt deleteDeviceInfoByIDStmt *sql.Stmt
deleteDeviceInfoLikeStmt *sql.Stmt deleteDeviceInfoLikeStmt *sql.Stmt
deleteScriptTriggerStmt *sql.Stmt
insertDeviceAliasStmt *sql.Stmt insertDeviceAliasStmt *sql.Stmt
listDeviceAliasesStmt *sql.Stmt listDeviceAliasesStmt *sql.Stmt
listDeviceAssignmentsStmt *sql.Stmt listDeviceAssignmentsStmt *sql.Stmt
listDeviceAuthStmt *sql.Stmt listDeviceAuthStmt *sql.Stmt
listDeviceInfosStmt *sql.Stmt listDeviceInfosStmt *sql.Stmt
listScriptTriggersStmt *sql.Stmt
listScriptVariablesStmt *sql.Stmt listScriptVariablesStmt *sql.Stmt
listScriptsStmt *sql.Stmt listScriptsStmt *sql.Stmt
replaceDeviceAssignmentStmt *sql.Stmt replaceDeviceAssignmentStmt *sql.Stmt
replaceDeviceAuthStmt *sql.Stmt replaceDeviceAuthStmt *sql.Stmt
replaceDeviceInfoStmt *sql.Stmt replaceDeviceInfoStmt *sql.Stmt
replaceScriptTriggerStmt *sql.Stmt
saveScriptStmt *sql.Stmt saveScriptStmt *sql.Stmt
updateScriptVariablesStmt *sql.Stmt updateScriptVariablesStmt *sql.Stmt
} }
@ -262,16 +289,19 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries {
deleteDeviceInfoStmt: q.deleteDeviceInfoStmt, deleteDeviceInfoStmt: q.deleteDeviceInfoStmt,
deleteDeviceInfoByIDStmt: q.deleteDeviceInfoByIDStmt, deleteDeviceInfoByIDStmt: q.deleteDeviceInfoByIDStmt,
deleteDeviceInfoLikeStmt: q.deleteDeviceInfoLikeStmt, deleteDeviceInfoLikeStmt: q.deleteDeviceInfoLikeStmt,
deleteScriptTriggerStmt: q.deleteScriptTriggerStmt,
insertDeviceAliasStmt: q.insertDeviceAliasStmt, insertDeviceAliasStmt: q.insertDeviceAliasStmt,
listDeviceAliasesStmt: q.listDeviceAliasesStmt, listDeviceAliasesStmt: q.listDeviceAliasesStmt,
listDeviceAssignmentsStmt: q.listDeviceAssignmentsStmt, listDeviceAssignmentsStmt: q.listDeviceAssignmentsStmt,
listDeviceAuthStmt: q.listDeviceAuthStmt, listDeviceAuthStmt: q.listDeviceAuthStmt,
listDeviceInfosStmt: q.listDeviceInfosStmt, listDeviceInfosStmt: q.listDeviceInfosStmt,
listScriptTriggersStmt: q.listScriptTriggersStmt,
listScriptVariablesStmt: q.listScriptVariablesStmt, listScriptVariablesStmt: q.listScriptVariablesStmt,
listScriptsStmt: q.listScriptsStmt, listScriptsStmt: q.listScriptsStmt,
replaceDeviceAssignmentStmt: q.replaceDeviceAssignmentStmt, replaceDeviceAssignmentStmt: q.replaceDeviceAssignmentStmt,
replaceDeviceAuthStmt: q.replaceDeviceAuthStmt, replaceDeviceAuthStmt: q.replaceDeviceAuthStmt,
replaceDeviceInfoStmt: q.replaceDeviceInfoStmt, replaceDeviceInfoStmt: q.replaceDeviceInfoStmt,
replaceScriptTriggerStmt: q.replaceScriptTriggerStmt,
saveScriptStmt: q.saveScriptStmt, saveScriptStmt: q.saveScriptStmt,
updateScriptVariablesStmt: q.updateScriptVariablesStmt, updateScriptVariablesStmt: q.updateScriptVariablesStmt,
} }

11
services/mysqldb/mysqlgen/models.go

@ -40,6 +40,17 @@ type Script struct {
Data json.RawMessage 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 { type ScriptVariable struct {
Scope string Scope string
Name string Name string

77
services/mysqldb/mysqlgen/script.sql.go

@ -8,8 +8,55 @@ package mysqlgen
import ( import (
"context" "context"
"encoding/json" "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 const listScriptVariables = `-- name: ListScriptVariables :many
SELECT scope, name, value FROM script_variable SELECT scope, name, value FROM script_variable
` `
@ -64,6 +111,36 @@ func (q *Queries) ListScripts(ctx context.Context) ([]Script, error) {
return items, nil 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 const saveScript = `-- name: SaveScript :exec
REPLACE INTO script (name, data) VALUES (?, ?) REPLACE INTO script (name, data) VALUES (?, ?)
` `

10
services/mysqldb/queries/script.sql

@ -9,3 +9,13 @@ SELECT * FROM script_variable;
-- name: UpdateScriptVariables :exec -- name: UpdateScriptVariables :exec
REPLACE INTO script_variable (scope, name, value) VALUES (?, ?, ?); 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 = ?;

72
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, Name: scr.Name,
Lines: lines, Lines: lines,
}) })
@ -174,6 +174,26 @@ func (d *database) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Event) {
bus.RunCommand(cmd) 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: case events.DeviceAccepted:
if event.Extras == nil { if event.Extras == nil {
event.Extras = map[string]string{} event.Extras = map[string]string{}
@ -359,6 +379,33 @@ func (d *database) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Comman
defer cancel() defer cancel()
switch command := command.(type) { 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: case script.SetVariable:
scopeName := "global" scopeName := "global"
if command.Match != nil { 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 { 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, Scope: scopeName,
Name: command.Key, Name: command.Key,
Value: command.Value, 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) j, _ := json.Marshal(command.Lines)
err := q.SaveScript(timeout, mysqlgen.SaveScriptParams{ 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) { func Connect(host string, port int, username, password, dbname string) (lucifer3.ActiveService, error) {
db, err := sql.Open("mysql", fmt.Sprintf( db, err := sql.Open("mysql", fmt.Sprintf(
"%s:%s@(%s:%d)/%s?parseTime=true", username, password, host, port, dbname, "%s:%s@(%s:%d)/%s?parseTime=true", username, password, host, port, dbname,

4
services/resolver.go

@ -56,6 +56,10 @@ func (r *Resolver) GetByID(id string) *device.Pointer {
} }
func (r *Resolver) Resolve(pattern string) []device.Pointer { func (r *Resolver) Resolve(pattern string) []device.Pointer {
if pattern == "" {
return []device.Pointer{}
}
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()

35
services/script/commands.go

@ -2,9 +2,26 @@ package script
import ( import (
"fmt" "fmt"
"github.com/google/uuid"
"strings" "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 { type SetVariable struct {
Match *string Match *string
Devices []string Devices []string
@ -15,28 +32,28 @@ type SetVariable struct {
func (c SetVariable) CommandDescription() string { func (c SetVariable) CommandDescription() string {
switch { switch {
case c.Match != nil: 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: 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: 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"` Name string `json:"name"`
Lines []Line `json:"lines"` 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"` Name string `json:"name"`
Match string `json:"match"` 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)
} }

40
services/script/service.go

@ -6,36 +6,51 @@ import (
"git.aiterp.net/lucifer3/server/device" "git.aiterp.net/lucifer3/server/device"
"git.aiterp.net/lucifer3/server/internal/formattools" "git.aiterp.net/lucifer3/server/internal/formattools"
"git.aiterp.net/lucifer3/server/internal/gentools" "git.aiterp.net/lucifer3/server/internal/gentools"
"github.com/google/uuid"
"sort" "sort"
"sync"
) )
type service struct { type service struct {
mu sync.Mutex
resolver device.Resolver resolver device.Resolver
variables *Variables variables *Variables
scripts map[string]*Script scripts map[string]*Script
triggers map[uuid.UUID]*Trigger
} }
func (s *service) Active() bool { func (s *service) Active() bool {
return true 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) { func (s *service) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command) {
switch command := command.(type) { switch command := command.(type) {
case UpdateScript:
s.mu.Lock()
case Update:
s.scripts[command.Name] = &Script{ s.scripts[command.Name] = &Script{
Name: command.Name, Name: command.Name,
Lines: command.Lines, Lines: command.Lines,
} }
s.mu.Unlock()
case ExecuteScript:
s.mu.Lock()
case Execute:
script := s.scripts[command.Name] script := s.scripts[command.Name]
s.mu.Unlock()
if script == nil { if script == nil {
break 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) 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{ return &service{
resolver: resolver, resolver: resolver,
variables: variables, variables: variables,
scripts: map[string]*Script{},
triggers: make(map[uuid.UUID]*Trigger, 8),
scripts: make(map[string]*Script, 8),
} }
} }

70
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
}

44
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(),
})
}
}()
}
}

2
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)}, Device: &DevicePatch{ID: id, DesiredState: gentools.ShallowCopy(&state)},
}) })
} }
case script.UpdateScript:
case script.Update:
patches = append(patches, Patch{ patches = append(patches, Patch{
Script: &ScriptPatch{Name: command.Name, Lines: command.Lines}, Script: &ScriptPatch{Name: command.Name, Lines: command.Lines},
}) })

Loading…
Cancel
Save